@aethervtt/shard-terrain 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,441 @@
|
|
|
1
|
+
import { hash32, quat } from "@aethervtt/shard-core";
|
|
2
|
+
import { loadNoiseKernel, NoiseGraph, NoiseGraphs } from "@aethervtt/shard-noise";
|
|
3
|
+
import {
|
|
4
|
+
CharacterController,
|
|
5
|
+
CharacterIntent,
|
|
6
|
+
CharacterState,
|
|
7
|
+
PhysicsConfig,
|
|
8
|
+
physics3dPlugin
|
|
9
|
+
} from "@aethervtt/shard-physics";
|
|
10
|
+
import {
|
|
11
|
+
Camera3d,
|
|
12
|
+
captureView,
|
|
13
|
+
DirectionalLight,
|
|
14
|
+
Exposure,
|
|
15
|
+
forwardPlugin,
|
|
16
|
+
Gpu,
|
|
17
|
+
OffscreenTarget,
|
|
18
|
+
RenderTargets,
|
|
19
|
+
renderPlugin,
|
|
20
|
+
Shaders,
|
|
21
|
+
Tonemapping
|
|
22
|
+
} from "@aethervtt/shard-render";
|
|
23
|
+
import { App } from "@aethervtt/shard-runtime";
|
|
24
|
+
import {
|
|
25
|
+
FloatingOrigin,
|
|
26
|
+
Grid,
|
|
27
|
+
placeInGrid,
|
|
28
|
+
Transform,
|
|
29
|
+
TransformPlugin,
|
|
30
|
+
worldPosition64
|
|
31
|
+
} from "@aethervtt/shard-transform";
|
|
32
|
+
import { TerrainBudget } from "../components.js";
|
|
33
|
+
import { lockCode } from "../grid-mesh.js";
|
|
34
|
+
import { TerrainWorld } from "../heights.js";
|
|
35
|
+
import { morphFactor } from "../lod.js";
|
|
36
|
+
import { terrainPlugin } from "../plugin.js";
|
|
37
|
+
import { tilesOf } from "./colliders.js";
|
|
38
|
+
import { Terrain } from "./component.js";
|
|
39
|
+
import { Heightmaps } from "./heightmap.js";
|
|
40
|
+
import { dequantize, LEAF_SIDE, PAGE, SIDE } from "./kernel.js";
|
|
41
|
+
import { nodeKey } from "./pages.js";
|
|
42
|
+
import { pageHeight } from "./queries.js";
|
|
43
|
+
import { parseTerrainSource, terrainLayout } from "./source.js";
|
|
44
|
+
import { TerrainSourceAsset, TerrainSources } from "./source-asset.js";
|
|
45
|
+
import { mainNoise } from "./stack.js";
|
|
46
|
+
function sourceAsset(world, o) {
|
|
47
|
+
const source = parseTerrainSource(o.source);
|
|
48
|
+
const layout = terrainLayout(source, o.block);
|
|
49
|
+
const deps = {};
|
|
50
|
+
const graphs = world.initResource(NoiseGraphs);
|
|
51
|
+
for (const [path, graph] of Object.entries(o.noise ?? {})) {
|
|
52
|
+
const ref = graphs.add(graph, path);
|
|
53
|
+
deps[path] = { guid: ref.guid, type: "NoiseGraph", hash: graph.hash };
|
|
54
|
+
}
|
|
55
|
+
const maps = world.initResource(Heightmaps);
|
|
56
|
+
for (const [path, map] of Object.entries(o.heightmaps ?? {})) {
|
|
57
|
+
const ref = maps.add({ ...map, version: 0 }, path);
|
|
58
|
+
deps[path] = { guid: ref.guid, type: "Heightmap", hash: path };
|
|
59
|
+
}
|
|
60
|
+
return new TerrainSourceAsset({
|
|
61
|
+
format: 1,
|
|
62
|
+
source,
|
|
63
|
+
layout,
|
|
64
|
+
deps,
|
|
65
|
+
hash: JSON.stringify([o.source, Object.keys(deps)])
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function heightfieldApp(gpu, o) {
|
|
69
|
+
const app = new App().addPlugin(TransformPlugin);
|
|
70
|
+
if (gpu) app.addPlugin(renderPlugin({ gpu, windowView: false }), forwardPlugin({ msaa: 1 }));
|
|
71
|
+
if (o.physics) app.addPlugin(physics3dPlugin());
|
|
72
|
+
app.addPlugin(terrainPlugin({ workers: o.workers, fs: o.fs }), ...o.extra ?? []);
|
|
73
|
+
await app.init();
|
|
74
|
+
const world = app.world;
|
|
75
|
+
if (o.physics) world.resource(PhysicsConfig).gravity = [0, -9.81, 0];
|
|
76
|
+
if (o.pages !== void 0) world.resource(TerrainBudget).pages = o.pages;
|
|
77
|
+
if (o.pagesPerFrame !== void 0) world.resource(TerrainBudget).pagesPerFrame = o.pagesPerFrame;
|
|
78
|
+
const asset = sourceAsset(world, o);
|
|
79
|
+
const sources = world.initResource(TerrainSources);
|
|
80
|
+
let ref = sources.add(asset, "test.terrain.json");
|
|
81
|
+
if (o.name) {
|
|
82
|
+
sources.set(o.name, asset);
|
|
83
|
+
ref = { type: "terrain/TerrainSource", guid: o.name, path: `${o.name}.terrain.json` };
|
|
84
|
+
}
|
|
85
|
+
const terrain = world.spawn(
|
|
86
|
+
[Grid, { cellSize: 2e3 }],
|
|
87
|
+
[
|
|
88
|
+
Terrain,
|
|
89
|
+
{
|
|
90
|
+
source: ref,
|
|
91
|
+
skirts: o.skirts ?? true,
|
|
92
|
+
errorPixels: o.errorPixels ?? 2,
|
|
93
|
+
...o.vertexPixels !== void 0 ? { vertexPixels: o.vertexPixels } : {}
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
[Transform, { translation: o.at ?? [0, 0, 0] }]
|
|
97
|
+
);
|
|
98
|
+
let view = "";
|
|
99
|
+
let camera = -1;
|
|
100
|
+
let sun = -1;
|
|
101
|
+
if (gpu) {
|
|
102
|
+
const target = new OffscreenTarget(gpu, {
|
|
103
|
+
label: "heightfield-test",
|
|
104
|
+
width: o.width ?? 96,
|
|
105
|
+
height: o.heightPx ?? 64
|
|
106
|
+
});
|
|
107
|
+
const targetRef = world.resource(RenderTargets).add(target, "heightfield-test");
|
|
108
|
+
camera = world.spawn(
|
|
109
|
+
[
|
|
110
|
+
Camera3d,
|
|
111
|
+
{
|
|
112
|
+
target: targetRef,
|
|
113
|
+
fovY: o.fovY ?? 60,
|
|
114
|
+
near: 0.05,
|
|
115
|
+
far: 1e6,
|
|
116
|
+
clearColor: o.clearColor ?? [1, 0, 1, 1]
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
[Exposure, { ev100: 14 }],
|
|
120
|
+
[Tonemapping, { curve: "none", dither: false }],
|
|
121
|
+
Transform,
|
|
122
|
+
FloatingOrigin
|
|
123
|
+
);
|
|
124
|
+
view = `camera:${camera}`;
|
|
125
|
+
sun = world.spawn(
|
|
126
|
+
[DirectionalLight, { illuminance: 5e4, shadows: false }],
|
|
127
|
+
[
|
|
128
|
+
Transform,
|
|
129
|
+
{
|
|
130
|
+
rotation: quat.fromEuler([0, 0, 0, 1], -0.8, 0.4, 0)
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
app.update(1 / 60);
|
|
136
|
+
return {
|
|
137
|
+
app,
|
|
138
|
+
world,
|
|
139
|
+
terrain,
|
|
140
|
+
camera,
|
|
141
|
+
sun,
|
|
142
|
+
view,
|
|
143
|
+
runtime: () => world.resource(TerrainWorld).heightfields.get(terrain),
|
|
144
|
+
render: () => world.resource(TerrainWorld).heightfields.get(terrain)?.parts.get("render")
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function lookAt(p, eye, target) {
|
|
148
|
+
const f = [target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]];
|
|
149
|
+
const rotation = quat.lookRotation([0, 0, 0, 1], f, [0, 1, 0]);
|
|
150
|
+
placeInGrid(p.world, p.camera, p.terrain, eye);
|
|
151
|
+
p.world.set(p.camera, Transform, { ...p.world.get(p.camera, Transform), rotation });
|
|
152
|
+
}
|
|
153
|
+
async function untilStreaming(p, maxMs = 12e4) {
|
|
154
|
+
const start = performance.now();
|
|
155
|
+
while (performance.now() - start < maxMs) {
|
|
156
|
+
p.app.update(1 / 60);
|
|
157
|
+
const rt = p.runtime();
|
|
158
|
+
if (rt?.problem) throw rt.problem;
|
|
159
|
+
if (rt?.streaming) return;
|
|
160
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
161
|
+
}
|
|
162
|
+
throw new Error(
|
|
163
|
+
`terrain not streaming after ${maxMs} ms (bake: ${p.runtime()?.bake}, waiting: ${p.runtime()?.waiting})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
async function settleHeightfield(p, maxFrames = 600) {
|
|
167
|
+
const gpu = p.world.tryResource(Gpu);
|
|
168
|
+
let quiet = 0;
|
|
169
|
+
for (let f = 0; f < maxFrames; f++) {
|
|
170
|
+
p.app.update(1 / 60);
|
|
171
|
+
const rt = p.runtime();
|
|
172
|
+
const r = p.render();
|
|
173
|
+
if (gpu) {
|
|
174
|
+
await p.world.resource(Shaders).whenIdle();
|
|
175
|
+
await gpu.pipelines.whenIdle();
|
|
176
|
+
}
|
|
177
|
+
await new Promise((res) => setTimeout(res, 1));
|
|
178
|
+
const busy = !rt?.streaming || rt.selection.requestedCount > 0 || (rt.pages?.pendingReads ?? 0) > 0 || (r ? r.arrived.length > 0 || r.pending.size > 0 : true) || (gpu ? gpu.pipelines.pending > 0 : false);
|
|
179
|
+
quiet = busy ? 0 : quiet + 1;
|
|
180
|
+
if (quiet >= 3) return f;
|
|
181
|
+
}
|
|
182
|
+
return maxFrames;
|
|
183
|
+
}
|
|
184
|
+
async function capture(p) {
|
|
185
|
+
const shot = captureView(p.world, p.view);
|
|
186
|
+
p.app.update(1 / 60);
|
|
187
|
+
return shot;
|
|
188
|
+
}
|
|
189
|
+
function holes(data, W, H, fov, eye, right, up, forward, lowest, rect) {
|
|
190
|
+
let count = 0;
|
|
191
|
+
const ty = Math.tan(fov / 2 * Math.PI / 180);
|
|
192
|
+
const tx = ty * (W / H);
|
|
193
|
+
for (let py = 0; py < H; py++) {
|
|
194
|
+
for (let px = 0; px < W; px++) {
|
|
195
|
+
const a = (px + 0.5) / W * 2 - 1;
|
|
196
|
+
const b = 1 - (py + 0.5) / H * 2;
|
|
197
|
+
const d = [0, 1, 2].map((k) => forward[k] + right[k] * a * tx + up[k] * b * ty);
|
|
198
|
+
if (d[1] >= -1e-6) continue;
|
|
199
|
+
const t = (lowest - eye[1]) / d[1];
|
|
200
|
+
if (t <= 0) continue;
|
|
201
|
+
const x = eye[0] + d[0] * t;
|
|
202
|
+
const z = eye[2] + d[2] * t;
|
|
203
|
+
if (x < rect[0] || z < rect[1] || x > rect[2] || z > rect[3]) continue;
|
|
204
|
+
const o = (py * W + px) * 4;
|
|
205
|
+
if (data[o] > 200 && data[o + 1] < 60 && data[o + 2] > 200) count++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return count;
|
|
209
|
+
}
|
|
210
|
+
function valleySource(size = 2048, spacing = 0.5) {
|
|
211
|
+
const bowl = new Float32Array(64 * 64);
|
|
212
|
+
for (let y = 0; y < 64; y++)
|
|
213
|
+
for (let x = 0; x < 64; x++)
|
|
214
|
+
bowl[y * 64 + x] = Math.max(0, 1 - Math.hypot(x - 31.5, y - 31.5) / 32);
|
|
215
|
+
return {
|
|
216
|
+
source: {
|
|
217
|
+
size: [size, size],
|
|
218
|
+
spacing,
|
|
219
|
+
heightRange: [-150, 350],
|
|
220
|
+
seed: 7,
|
|
221
|
+
splines: {
|
|
222
|
+
road: {
|
|
223
|
+
points: [
|
|
224
|
+
[size * 0.05, "ground", size * 0.1],
|
|
225
|
+
[size * 0.45, "ground", size * 0.35],
|
|
226
|
+
[size * 0.7, "ground", size * 0.85]
|
|
227
|
+
],
|
|
228
|
+
width: 8,
|
|
229
|
+
falloff: 14
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
height: [
|
|
233
|
+
{ noise: { path: "hills" }, scale: 80 },
|
|
234
|
+
{
|
|
235
|
+
image: { path: "valley" },
|
|
236
|
+
at: [size * 0.6, size * 0.45],
|
|
237
|
+
size: [size * 0.3, size * 0.22],
|
|
238
|
+
rotation: 20,
|
|
239
|
+
range: [0, -60],
|
|
240
|
+
blend: "add",
|
|
241
|
+
falloff: 80
|
|
242
|
+
},
|
|
243
|
+
{ spline: "road", mode: "flatten" }
|
|
244
|
+
],
|
|
245
|
+
layers: [{ name: "grass" }, { name: "rock", triplanar: true }, { name: "gravel" }],
|
|
246
|
+
paint: [
|
|
247
|
+
{ layer: "grass" },
|
|
248
|
+
{ layer: "rock", slope: [32, 90], blend: 6 },
|
|
249
|
+
{ layer: "gravel", spline: "road", blend: 2 }
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
heightmaps: { valley: { width: 64, height: 64, data: bowl } }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const VALLEY_HILLS = {
|
|
256
|
+
output: "h",
|
|
257
|
+
nodes: {
|
|
258
|
+
base: { fbm: { source: "simplex", octaves: 6, frequency: 12e-4, seed: 1 } },
|
|
259
|
+
ridges: { ridged: { source: "simplex", octaves: 4, frequency: 5e-3, seed: 2 } },
|
|
260
|
+
h: { add: ["base", { multiply: ["ridges", 0.12] }] }
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
function snapshotDrawn(p) {
|
|
264
|
+
const rt = p.runtime();
|
|
265
|
+
const r = p.render();
|
|
266
|
+
const t = rt.tree;
|
|
267
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
268
|
+
const g = new Float64Array(2);
|
|
269
|
+
for (let i = 0; i < rt.selection.renderedCount; i++) {
|
|
270
|
+
const n = rt.selection.rendered[i];
|
|
271
|
+
rt.grid.globalOf(t, n, g);
|
|
272
|
+
const slot = t.slot[n];
|
|
273
|
+
nodes.set(nodeKey(t.depth[n], g[0], g[1]), {
|
|
274
|
+
depth: t.depth[n],
|
|
275
|
+
x: g[0],
|
|
276
|
+
z: g[1],
|
|
277
|
+
locks: Array.from(t.locks.subarray(n * 4, n * 4 + 4)),
|
|
278
|
+
mask: t.mask[n],
|
|
279
|
+
fade: slot >= 0 ? r.table[slot * 4 + 2] / 255 : 0,
|
|
280
|
+
anchored: slot >= 0 && r.table[slot * 4 + 3] >= 128
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
nodes,
|
|
285
|
+
camera: Array.from(r.view.position),
|
|
286
|
+
splitScale: r.camera[3],
|
|
287
|
+
errors: Float32Array.from(rt.errors)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function drawnHeightAt(rt, f, x, z, skipStitched = true) {
|
|
291
|
+
const layout = rt.layout;
|
|
292
|
+
for (let depth = layout.depth; depth >= 0; depth--) {
|
|
293
|
+
const size = rt.nodeSize(depth);
|
|
294
|
+
const nx = Math.floor(x / size);
|
|
295
|
+
const nz = Math.floor(z / size);
|
|
296
|
+
const node = f.nodes.get(nodeKey(depth, nx, nz));
|
|
297
|
+
if (!node) continue;
|
|
298
|
+
const page = rt.pages?.get(nodeKey(depth, nx, nz));
|
|
299
|
+
if (!page) return void 0;
|
|
300
|
+
const side = page.leaf ? LEAF_SIDE : SIDE;
|
|
301
|
+
const off = page.leaf ? 1 : 0;
|
|
302
|
+
const spacing = size / PAGE;
|
|
303
|
+
const fx = (x - nx * size) / spacing;
|
|
304
|
+
const fz = (z - nz * size) / spacing;
|
|
305
|
+
const ci = Math.min(PAGE - 1, Math.floor(fx));
|
|
306
|
+
const cj = Math.min(PAGE - 1, Math.floor(fz));
|
|
307
|
+
if (skipStitched) {
|
|
308
|
+
const near = [cj === 0, ci === PAGE - 1, cj === PAGE - 1, ci === 0];
|
|
309
|
+
for (let e = 0; e < 4; e++) if (near[e] && node.locks[e] === 1) return void 0;
|
|
310
|
+
}
|
|
311
|
+
const h = (i, j) => dequantize(page.heights[(j + off) * side + i + off], rt.lo, rt.hi);
|
|
312
|
+
const vertex = (i, j) => {
|
|
313
|
+
const own = h(i, j);
|
|
314
|
+
const oi = (i & 1) === 1;
|
|
315
|
+
const oj = (j & 1) === 1;
|
|
316
|
+
const parent = oi && oj ? 0.5 * (h(i + 1, j - 1) + h(i - 1, j + 1)) : oi ? 0.5 * (h(i - 1, j) + h(i + 1, j)) : oj ? 0.5 * (h(i, j - 1) + h(i, j + 1)) : own;
|
|
317
|
+
const wx = nx * size + i * spacing;
|
|
318
|
+
const wz = nz * size + j * spacing;
|
|
319
|
+
const split = depth === 0 || node.anchored ? 0 : f.errors[depth] * f.splitScale;
|
|
320
|
+
const d = Math.hypot(wx - f.camera[0], own - f.camera[1], wz - f.camera[2]);
|
|
321
|
+
let t = morphFactor(d, split);
|
|
322
|
+
const code = lockCode(
|
|
323
|
+
i,
|
|
324
|
+
j,
|
|
325
|
+
PAGE + 1,
|
|
326
|
+
i === 0 || j === 0 || i === PAGE || j === PAGE ? ringIndex(i, j) : PAGE * 4,
|
|
327
|
+
PAGE * 4
|
|
328
|
+
);
|
|
329
|
+
if (code >= 0 && code < 4) {
|
|
330
|
+
const lock = node.locks[code];
|
|
331
|
+
if (lock >= 0) t = lock === 1 ? 1 : 0;
|
|
332
|
+
} else if (code >= 4 && node.mask !== 15) {
|
|
333
|
+
const [qa, qb] = code === 5 ? [2, 3] : code === 6 ? [0, 2] : code === 7 ? [1, 3] : [0, 1];
|
|
334
|
+
const differ = (node.mask >> qa & 1) !== (node.mask >> qb & 1);
|
|
335
|
+
t = differ || code === 8 && node.mask !== 0 ? 0 : Math.max(t, node.fade);
|
|
336
|
+
} else t = Math.max(t, node.fade);
|
|
337
|
+
return own + (parent - own) * t;
|
|
338
|
+
};
|
|
339
|
+
const u = fx - ci;
|
|
340
|
+
const v = fz - cj;
|
|
341
|
+
const h00 = vertex(ci, cj);
|
|
342
|
+
const h10 = vertex(ci + 1, cj);
|
|
343
|
+
const h01 = vertex(ci, cj + 1);
|
|
344
|
+
const h11 = vertex(ci + 1, cj + 1);
|
|
345
|
+
return u + v <= 1 ? h00 + u * (h10 - h00) + v * (h01 - h00) : h11 + (1 - u) * (h01 - h11) + (1 - v) * (h10 - h11);
|
|
346
|
+
}
|
|
347
|
+
return void 0;
|
|
348
|
+
}
|
|
349
|
+
function ringIndex(i, j) {
|
|
350
|
+
const s = PAGE;
|
|
351
|
+
if (j === 0 && i < s) return i;
|
|
352
|
+
if (i === s && j < s) return s + j;
|
|
353
|
+
if (j === s && i > 0) return 2 * s + (s - i);
|
|
354
|
+
return 3 * s + (s - j);
|
|
355
|
+
}
|
|
356
|
+
async function heightfieldWalk(characters = 4, seconds = 3, workers) {
|
|
357
|
+
const t0 = performance.now();
|
|
358
|
+
await loadNoiseKernel();
|
|
359
|
+
const hills = await NoiseGraph.create(VALLEY_HILLS);
|
|
360
|
+
const p = await heightfieldApp(void 0, {
|
|
361
|
+
...valleySource(),
|
|
362
|
+
noise: { hills },
|
|
363
|
+
physics: true,
|
|
364
|
+
...workers ? { workers } : {}
|
|
365
|
+
});
|
|
366
|
+
const w = p.world;
|
|
367
|
+
const rt = p.runtime();
|
|
368
|
+
const walked = [];
|
|
369
|
+
const bytes = new Uint8Array(characters * 24 + 4);
|
|
370
|
+
const f64 = new Float64Array(bytes.buffer, 0, characters * 3);
|
|
371
|
+
const p0 = new Float64Array(3);
|
|
372
|
+
const pos = new Float64Array(3);
|
|
373
|
+
let lowest = Infinity;
|
|
374
|
+
const ground = (x, z) => {
|
|
375
|
+
const size = rt.layout.leafSize;
|
|
376
|
+
const page = rt.pages.leafNow(
|
|
377
|
+
mainNoise(),
|
|
378
|
+
rt.stack,
|
|
379
|
+
Math.floor(x / size),
|
|
380
|
+
Math.floor(z / size)
|
|
381
|
+
);
|
|
382
|
+
return pageHeight(rt, page, x, z);
|
|
383
|
+
};
|
|
384
|
+
for (let k = 0; k < characters; k++) {
|
|
385
|
+
const x = 200 + hash32(71, k) / 2 ** 32 * 1600;
|
|
386
|
+
const z = 400 + hash32(72, k) / 2 ** 32 * 1400;
|
|
387
|
+
const c = w.spawn(
|
|
388
|
+
[CharacterController, { radius: 0.35, height: 1.8 }],
|
|
389
|
+
[CharacterIntent, {}],
|
|
390
|
+
[CharacterState, {}],
|
|
391
|
+
Transform,
|
|
392
|
+
FloatingOrigin
|
|
393
|
+
);
|
|
394
|
+
placeInGrid(w, c, p.terrain, [x, ground(x, z) + 1.2, z]);
|
|
395
|
+
for (let f = 0; f < 60; f++) p.app.update(1 / 60);
|
|
396
|
+
worldPosition64(w, c, p0, p.terrain);
|
|
397
|
+
w.set(c, CharacterIntent, { move: [0, 0, -5] });
|
|
398
|
+
for (let f = 0; f < seconds * 60; f++) {
|
|
399
|
+
p.app.update(1 / 60);
|
|
400
|
+
worldPosition64(w, c, pos, p.terrain);
|
|
401
|
+
if (pos[0] > 0 && pos[2] > 0 && pos[0] < 2048 && pos[2] < 2048)
|
|
402
|
+
lowest = Math.min(lowest, pos[1] - 0.9 - ground(pos[0], pos[2]));
|
|
403
|
+
}
|
|
404
|
+
walked.push(Math.hypot(pos[0] - p0[0], pos[2] - p0[2]));
|
|
405
|
+
f64.set(pos, k * 3);
|
|
406
|
+
w.despawn(c);
|
|
407
|
+
}
|
|
408
|
+
const tiles = tilesOf(rt).tiles.size;
|
|
409
|
+
new DataView(bytes.buffer).setUint32(characters * 24, tiles, true);
|
|
410
|
+
let hash = 2166136261;
|
|
411
|
+
for (let i = 0; i < bytes.length; i++) hash = Math.imul(hash ^ bytes[i], 16777619);
|
|
412
|
+
await p.app.dispose();
|
|
413
|
+
return {
|
|
414
|
+
checksum: (hash >>> 0).toString(16).padStart(8, "0"),
|
|
415
|
+
walked,
|
|
416
|
+
lowest,
|
|
417
|
+
tiles,
|
|
418
|
+
ms: performance.now() - t0
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function openWorldSource() {
|
|
422
|
+
const v = valleySource(16384, 1);
|
|
423
|
+
const height = v.source.height;
|
|
424
|
+
height[0] = { noise: { path: "hills" }, scale: 260 };
|
|
425
|
+
return { ...v, source: { ...v.source, heightRange: [-300, 700] } };
|
|
426
|
+
}
|
|
427
|
+
export {
|
|
428
|
+
VALLEY_HILLS,
|
|
429
|
+
capture,
|
|
430
|
+
drawnHeightAt,
|
|
431
|
+
heightfieldApp,
|
|
432
|
+
heightfieldWalk,
|
|
433
|
+
holes,
|
|
434
|
+
lookAt,
|
|
435
|
+
openWorldSource,
|
|
436
|
+
settleHeightfield,
|
|
437
|
+
snapshotDrawn,
|
|
438
|
+
sourceAsset,
|
|
439
|
+
untilStreaming,
|
|
440
|
+
valleySource
|
|
441
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Pool jobs for heightfield terrain (spec 0071). Plain JavaScript: workers import it with no bundler
|
|
2
|
+
// or loader. The noise kernel comes by URL with its compiled module, so a worker samples exactly as
|
|
3
|
+
// the main thread does.
|
|
4
|
+
import { bakeBlock as bake, LEAF_SIDE, leafNormals, pageTexels, readPage, SIDE } from './kernel.js'
|
|
5
|
+
|
|
6
|
+
const kernels = new Map()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This worker's noise kernel: imported once per URL and instantiated from the first module posted
|
|
10
|
+
* (every job posts the same build; a posted module arrives as a new object each time).
|
|
11
|
+
*/
|
|
12
|
+
function noiseFor(url, module) {
|
|
13
|
+
let k = kernels.get(url)
|
|
14
|
+
if (!k) {
|
|
15
|
+
k = import(/* @vite-ignore */ url).then((m) => ({
|
|
16
|
+
state: m.instantiate(module),
|
|
17
|
+
computeOrigins: m.computeOrigins,
|
|
18
|
+
evalProgram: m.evalProgram,
|
|
19
|
+
}))
|
|
20
|
+
kernels.set(url, k)
|
|
21
|
+
}
|
|
22
|
+
return k
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Bakes one block (kernel.js bakeBlock) on this thread. */
|
|
26
|
+
export async function bakeBlock(url, module, stack, layout, bx, bz) {
|
|
27
|
+
return bake(await noiseFor(url, module), stack, layout, bx, bz)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inflates and decodes a page, and lays it out as the GPU pool holds it: what streaming needs
|
|
32
|
+
* from a read, off the main thread. Leaves' normals come from their heights here.
|
|
33
|
+
*/
|
|
34
|
+
export function loadPage(packed, leaf, cells, lo, hi, spacing) {
|
|
35
|
+
const page = readPage(packed, leaf, cells)
|
|
36
|
+
const normals =
|
|
37
|
+
page.normals ?? leafNormals(page.heights, lo, hi, spacing, new Uint8Array(SIDE * SIDE * 2))
|
|
38
|
+
const texels = pageTexels(
|
|
39
|
+
{ heights: page.heights, normals, control: page.control },
|
|
40
|
+
leaf,
|
|
41
|
+
lo,
|
|
42
|
+
hi,
|
|
43
|
+
spacing,
|
|
44
|
+
new Uint8Array(LEAF_SIDE * LEAF_SIDE * 4),
|
|
45
|
+
)
|
|
46
|
+
return { heights: page.heights, normals, control: page.control, texels }
|
|
47
|
+
}
|
package/dist/heights.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
-
import type { Workers } from '@aethervtt/shard-platform';
|
|
2
|
+
import type { PlatformFileSystem, Workers } from '@aethervtt/shard-platform';
|
|
3
|
+
import type { HeightfieldRuntime } from './heightfield/runtime.js';
|
|
3
4
|
import type { PlanetRuntime } from './planet.js';
|
|
4
|
-
/** Every planet's runtime state, and the frame counter terrain systems stamp with. */
|
|
5
|
+
/** Every planet's and heightfield's runtime state, and the frame counter terrain systems stamp with. */
|
|
5
6
|
export declare class TerrainState {
|
|
6
7
|
readonly planets: Map<number, PlanetRuntime>;
|
|
8
|
+
/** Heightfield terrains (0071), by `Terrain` entity. */
|
|
9
|
+
readonly heightfields: Map<number, HeightfieldRuntime>;
|
|
7
10
|
frame: number;
|
|
8
11
|
workers: Workers | undefined;
|
|
12
|
+
/** Where heightfield packs are read and baked (`.shard/cache/terrain`); memory without one. */
|
|
13
|
+
fs: PlatformFileSystem | undefined;
|
|
9
14
|
/** Set when a graph or biome asset (re)loaded: planets re-resolve their assets. */
|
|
10
15
|
dirty: boolean;
|
|
11
16
|
}
|
|
12
|
-
export declare const
|
|
17
|
+
export declare const TerrainWorld: import("@aethervtt/shard-core").ResourceDef<TerrainState>;
|
|
13
18
|
/** The runtime for a planet entity, ready to sample, or a `terrain/not-ready` error. */
|
|
14
19
|
export declare function planetRuntime(world: World, planet: Entity): PlanetRuntime;
|
|
15
20
|
/** Height-graph metres above radius at a unit direction in the planet's frame. Sync, CPU. */
|
package/dist/heights.js
CHANGED
|
@@ -3,17 +3,21 @@ import { biomeWeights, dominantBiome, MAX_BIOMES } from "./biomes.js";
|
|
|
3
3
|
import { samplePoint } from "./points.js";
|
|
4
4
|
class TerrainState {
|
|
5
5
|
planets = /* @__PURE__ */ new Map();
|
|
6
|
+
/** Heightfield terrains (0071), by `Terrain` entity. */
|
|
7
|
+
heightfields = /* @__PURE__ */ new Map();
|
|
6
8
|
frame = 0;
|
|
7
9
|
workers;
|
|
10
|
+
/** Where heightfield packs are read and baked (`.shard/cache/terrain`); memory without one. */
|
|
11
|
+
fs;
|
|
8
12
|
/** Set when a graph or biome asset (re)loaded: planets re-resolve their assets. */
|
|
9
13
|
dirty = true;
|
|
10
14
|
}
|
|
11
|
-
const
|
|
12
|
-
description: "
|
|
15
|
+
const TerrainWorld = defineResource("terrain/World", {
|
|
16
|
+
description: "Terrain state per planet and heightfield: quadtrees, pages, collider chunks, and resolved assets.",
|
|
13
17
|
init: () => new TerrainState()
|
|
14
18
|
});
|
|
15
19
|
function planetRuntime(world, planet) {
|
|
16
|
-
const rt = world.tryResource(
|
|
20
|
+
const rt = world.tryResource(TerrainWorld)?.planets.get(planet);
|
|
17
21
|
if (!rt) {
|
|
18
22
|
throw new ShardError("terrain/not-a-planet", `Entity ${planet} has no terrain/Planet`, {
|
|
19
23
|
hint: "Add terrain/Planet (and a Grid) to the planet entity, and the terrain plugin to the app."
|
|
@@ -127,8 +131,8 @@ function terrainSample(rt, direction) {
|
|
|
127
131
|
};
|
|
128
132
|
}
|
|
129
133
|
export {
|
|
130
|
-
Terrain,
|
|
131
134
|
TerrainState,
|
|
135
|
+
TerrainWorld,
|
|
132
136
|
heightAt,
|
|
133
137
|
planetHeightAt,
|
|
134
138
|
planetRuntime,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
export { Biome, type BiomeInputs, BiomeSet, type BiomeSetValue, type BiomeTable, type BiomeValue, BLENDED_BIOMES, biomeTable, biomeWeights, biomeWindow, dominantBiome, effectiveTemperature, MAX_BIOMES, MAX_LAYERS, } from './biomes.js';
|
|
2
|
-
export { assembleChunk, buildChunk, type
|
|
3
|
-
export { COLLIDER_DELAY, type ColliderChunk, ColliderSet, collidersOf, skirtDepth, } from './colliders.js';
|
|
2
|
+
export { assembleChunk, buildChunk, type ChunkMesh, type ChunkSpec, chunkCenter, } from './chunk.js';
|
|
3
|
+
export { type ChunkSampling, COLLIDER_DELAY, type ColliderChunk, ColliderSet, chunkSpec, collidersOf, sampleChunkAsync, skirtDepth, } from './colliders.js';
|
|
4
4
|
export { Chunk, Planet, PlanetNav, TerrainAnchor, TerrainBudget, type TerrainBudgetValue, } from './components.js';
|
|
5
5
|
export { directionToFace, EDGE_BOTTOM, EDGE_LEFT, EDGE_RIGHT, EDGE_TOP, faceToDirection, keyString, MAX_DEPTH, MAX_RADIUS, maxDepthFor, type NodeAddress, neighborNode, nodeAt, nodeExtent, nodeSpacing, packKey, parseKey, unpackKey, } from './cube.js';
|
|
6
|
+
export { CubeSphere, NodeTree } from './cube-sphere.js';
|
|
6
7
|
export { PlanetFrame } from './frame.js';
|
|
7
|
-
export {
|
|
8
|
+
export { type ChunkLayout, checkResolution, chunkIndices, chunkLayout, lockCode, } from './grid-mesh.js';
|
|
9
|
+
export { type BakeInput, type BakeManifest, type BakeOptions, type BakeReport, type BlockRecord, bakeTerrain, blockKeys, type OutOfRange, packHash, readManifest, type TerrainStats, terrainStats, } from './heightfield/bake.js';
|
|
10
|
+
export { type ColliderTile, type TileSet, tilesOf, } from './heightfield/colliders.js';
|
|
11
|
+
export { Terrain, TerrainChunk } from './heightfield/component.js';
|
|
12
|
+
export { decodeHeightPng, type Heightmap, HeightmapAssetType, HeightmapImporter, Heightmaps, } from './heightfield/heightmap.js';
|
|
13
|
+
export { BAKE_VERSION, BLOCK, type DecodedPage, deflate, inflate, LEAF_SIDE, PAGE, SIDE, type Stack, } from './heightfield/kernel.js';
|
|
14
|
+
export { TerrainSurfaceMaterial } from './heightfield/material.js';
|
|
15
|
+
export { fsPackStore, memoryPackStore, PACK, type PackEntry, type PackIndex, type PackStore, packOf, readIndex, writePack, } from './heightfield/pack.js';
|
|
16
|
+
export { type BakeHost, bakeIsCurrent, bakeProjectTerrains, type ProjectBake, stackFor, terrainCacheDir, } from './heightfield/project.js';
|
|
17
|
+
export { finestPage, type HeightfieldSample, heightfieldRuntime, heightfieldSample, loadTerrainRegion, pageHeight, type TerrainHeight, terrainHeightAt, } from './heightfield/queries.js';
|
|
18
|
+
export { HeightfieldDebug, type HeightfieldRender } from './heightfield/render.js';
|
|
19
|
+
export { HeightfieldRuntime } from './heightfield/runtime.js';
|
|
20
|
+
export { MAX_MATERIAL_LAYERS, parseTerrainSource, type SourceHeightLayer, type SourcePaint, type SourceSpline, sourceDependencies, type TerrainLayout, type TerrainSource, terrainLayout, } from './heightfield/source.js';
|
|
21
|
+
export { type SourceDependency, type TerrainSourceArtifact, TerrainSourceAsset, TerrainSourceAssetType, TerrainSourceImporter, TerrainSources, terrainJsonSchema, } from './heightfield/source-asset.js';
|
|
22
|
+
export { compileStack, mainNoise, type StackAssets } from './heightfield/stack.js';
|
|
23
|
+
export { heightfieldUpdates, updateHeightfields } from './heightfield/system.js';
|
|
24
|
+
export { heightAt, type PlanetSurface, planetHeightAt, planetRuntime, planetSurfaceAt, type TerrainSample, TerrainState, TerrainWorld, terrainSample, } from './heights.js';
|
|
8
25
|
export { GEN_CLIMATE, GEN_HEIGHT, GEN_OCEAN, GEN_ROOT, type KernelGraph, kernelGraph, PARAMS_BYTES, POINT_FLOATS, sampleKernel, VERTEX_KERNEL, } from './kernel.js';
|
|
9
|
-
export { measureErrors } from './lod.js';
|
|
26
|
+
export { adaptLodBias, capErrors, MAX_LOD_BIAS, MORPH_WGSL, measureErrors, morphFactor, } from './lod.js';
|
|
10
27
|
export { OceanMaterial, PlanetMaterial, TERRAIN_SHADERS, TEXTURE_PERIOD } from './material.js';
|
|
11
28
|
export { resolvePlanet, terrainMap, terrainMethods } from './methods.js';
|
|
12
29
|
export { clearPlanetNav, planetAgents, planetNavMesh, updatePlanetNav } from './nav.js';
|
|
13
|
-
export { terrainBiomesOverlay, terrainCollidersOverlay, terrainLodOverlay } from './overlays.js';
|
|
30
|
+
export { terrainBiomesOverlay, terrainCollidersOverlay, terrainLodOverlay, terrainPagesOverlay, } from './overlays.js';
|
|
14
31
|
export { COLLIDER_SPACING, PlanetRuntime, type PlanetSettings } from './planet.js';
|
|
15
32
|
export { type TerrainPluginOptions, terrainPlugin, updatePlanets } from './plugin.js';
|
|
16
|
-
export { type ChunkPoints, createChunkPoints, gridDirection, prepareChunkPoints, SNAP, sampleChunkPoints, samplePoint, } from './points.js';
|
|
17
|
-
export { createSelection, NODE_BOUNDS, NODE_READY,
|
|
33
|
+
export { type ChunkPoints, createChunkPoints, gridDirection, NOISE_OFFSET, prepareChunkPoints, SNAP, sampleChunkPoints, samplePoint, } from './points.js';
|
|
34
|
+
export { createSelection, NODE_BOUNDS, NODE_READY, type QuadSurface, QuadTree, type Selection, type SelectionParams, type SelectionView, selectNodes, } from './quadtree.js';
|
|
18
35
|
export { PlanetRender, renderOf, selectChunks, TerrainDebug } from './render.js';
|
|
19
36
|
export { createView, omniView, perspectiveView } from './view.js';
|