@threenative/core 0.3.3 → 0.3.4
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/capabilities.json +555 -21
- package/dist/{assets-CYKk2WTu.d.ts → assets-CqvE429w.d.ts} +45 -3
- package/dist/{canvas-layer-C1SnMoJ-.d.ts → canvas-layer-DDmC_VVF.d.ts} +1 -1
- package/dist/{game-D_6r-k4Y.d.ts → game-CljaDv4D.d.ts} +144 -10
- package/dist/{gpu-readback-CMklJs6r.d.ts → gpu-readback-CqJEfWNQ.d.ts} +16 -6
- package/dist/hot.d.ts +4 -4
- package/dist/index.d.ts +467 -35
- package/dist/index.js +3208 -257
- package/dist/playtest.d.ts +11 -5
- package/dist/playtest.js +55 -5
- package/dist/react.d.ts +2 -2
- package/dist/{renderer-Cy4qeBOA.d.ts → renderer-CfsS2hxi.d.ts} +190 -2
- package/dist/ui-layer.d.ts +5 -3
- package/dist/world.d.ts +900 -14
- package/dist/world.js +10876 -126
- package/gpl/fixtures/make_world_fixture.py +184 -0
- package/gpl/recipes/_common.py +11 -0
- package/gpl/recipes/decimate.py +11 -5
- package/gpl/recipes/export_world.py +682 -0
- package/mcp/blender-server.mjs +55 -1
- package/mcp/engine-server.mjs +54 -20
- package/mcp/servers.mjs +3 -3
- package/package.json +6 -6
- package/patches/three@0.185.1.patch +2101 -138
- package/scripts/apply-three-patch.mjs +61 -37
package/dist/world.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Object3D, Mesh, LOD, Vector3, Group, BufferGeometry } from 'three';
|
|
2
|
-
import { I as IComputeDriven, a as IGPUReadbackSample } from './gpu-readback-
|
|
3
|
-
import { I as IRendererLike } from './renderer-
|
|
4
|
-
import { I as IAssetLoader } from './assets-
|
|
1
|
+
import { Object3D, Mesh, LOD, Vector3, Material, Group, Camera, BufferGeometry } from 'three';
|
|
2
|
+
import { I as IComputeDriven, a as IGPUReadbackSample } from './gpu-readback-CqJEfWNQ.js';
|
|
3
|
+
import { I as IRendererLike } from './renderer-CfsS2hxi.js';
|
|
4
|
+
import { I as IAssetLoader } from './assets-CqvE429w.js';
|
|
5
5
|
import 'three/webgpu';
|
|
6
6
|
|
|
7
7
|
interface IWorldErosionOptions {
|
|
@@ -25,9 +25,27 @@ interface IWorldTileColliderInput {
|
|
|
25
25
|
readonly tileX: number;
|
|
26
26
|
readonly tileZ: number;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* One frame's allowance for admission work — the millisecond budget every path that puts streamed
|
|
30
|
+
* content on screen draws on, spent in bounded units rather than in one lump.
|
|
31
|
+
*
|
|
32
|
+
* `admit` is the whole contract: run one unit and charge the frame for it, or report that there was
|
|
33
|
+
* no room and run nothing. A caller that is refused leaves the work for a later frame instead of
|
|
34
|
+
* dropping it, so the budget decides *when* content is admitted, never *whether*.
|
|
35
|
+
*
|
|
36
|
+
* Nothing here decides what anything looks like; it decides only how much of it a frame may pay for.
|
|
37
|
+
*/
|
|
38
|
+
interface IAdmissionBudget {
|
|
39
|
+
/**
|
|
40
|
+
* Run one unit of admission work, charging the frame's budget for it. `false` means the budget
|
|
41
|
+
* was spent and `work` was never called, so the caller must stop and resume on a later frame.
|
|
42
|
+
*/
|
|
43
|
+
admit(work: () => void): boolean;
|
|
44
|
+
}
|
|
28
45
|
interface IWorldTile {
|
|
29
46
|
readonly bytes: number;
|
|
30
|
-
|
|
47
|
+
/** `undefined` for a resident tile outside `colliderRadius`, or when no factory was given. */
|
|
48
|
+
readonly collider: IWorldTileCollider | undefined;
|
|
31
49
|
readonly field: Heightfield;
|
|
32
50
|
readonly key: string;
|
|
33
51
|
readonly lod: LOD;
|
|
@@ -53,6 +71,14 @@ interface IWorldTilesOptions {
|
|
|
53
71
|
readonly assets?: Pick<IAssetLoader, "release">;
|
|
54
72
|
/** A game-owned, already-loaded logical model key, or a key resolver per tile. */
|
|
55
73
|
readonly assetKey?: string | ((tileX: number, tileZ: number) => string);
|
|
74
|
+
/**
|
|
75
|
+
* Chebyshev radius, in tiles from the followed one, that gets a `createCollider` body. Defaults
|
|
76
|
+
* to `streamRadius`, so every resident tile collides. A smaller radius keeps a wide render ring
|
|
77
|
+
* cheap to simulate: a tile crossing the radius has its collider created or disposed as it goes.
|
|
78
|
+
*/
|
|
79
|
+
readonly colliderRadius?: number;
|
|
80
|
+
/** Terrain tiles and seam bridges receive the scene's shadows. Default false. */
|
|
81
|
+
readonly receiveShadow?: boolean;
|
|
56
82
|
/** Creates the physics body from the field's explicit collider-order copy. */
|
|
57
83
|
readonly createCollider?: (input: IWorldTileColliderInput) => IWorldTileCollider;
|
|
58
84
|
/** TSL pass options are game supplied and are forwarded to each resident field. */
|
|
@@ -72,6 +98,20 @@ interface IWorldTilesOptions {
|
|
|
72
98
|
readonly lodFactors?: readonly number[];
|
|
73
99
|
/** Distances in world units at which the next LOD becomes active. */
|
|
74
100
|
readonly lodDistances?: readonly number[];
|
|
101
|
+
/**
|
|
102
|
+
* Re-derive the terrain every frame and assert it: a finiteness scan of every rendered vertex,
|
|
103
|
+
* a seam measurement per resident pair and an LOD pop sample per blending tile. Off by default
|
|
104
|
+
* because it is a measurement and it cost ~270 ms over a six-second walk on a 289-tile ring.
|
|
105
|
+
* `TN_TERRAIN_VALIDATE=1` or `?tnTerrainValidate=1` turns it on for a run; this overrides that.
|
|
106
|
+
*/
|
|
107
|
+
readonly validate?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Merge settled same-LOD terrain tiles into super-tiles: one mesh per K×K block, rebuilt from the
|
|
110
|
+
* resident tiles through the admission budget. It changes how many draws the main pass submits,
|
|
111
|
+
* never how the ground looks — a merged vertex lands on the world position its tile's vertex did.
|
|
112
|
+
* Off by default; `mergeTiles: true`, `?tnTerrainMerge=1` or `TN_TERRAIN_MERGE=1` turns it on.
|
|
113
|
+
*/
|
|
114
|
+
readonly mergeTiles?: boolean;
|
|
75
115
|
/** Explicit game-owned measurement region used by the topology evaluator. */
|
|
76
116
|
readonly topologyObservation?: IWorldTilesTopologyObservation;
|
|
77
117
|
}
|
|
@@ -86,7 +126,8 @@ interface IWorldTilesOptions {
|
|
|
86
126
|
* @alias stream terrain across chunks
|
|
87
127
|
* @constraint sampleHeight and surface are required game choices; no landform or surface preset is installed
|
|
88
128
|
* @constraint residentTileBudget and residentByteBudget are hard caps; a tile that cannot fit throws
|
|
89
|
-
* @
|
|
129
|
+
* @constraint seam gap, LOD pop and the rendered-vertex finiteness scan are measurements that are off by default; TN_TERRAIN_VALIDATE=1, ?tnTerrainValidate=1 or validate: true runs them, and maxSeamGap, maxVisualSeamGap and maxLodPop report undefined while they are off
|
|
130
|
+
* @override tileSize, tileResolution, lodFactors, lodDistances, skirtDepth, streamRadius, colliderRadius, validate, and budgets
|
|
90
131
|
* @example const tiles = new TerrainTiles({ sampleHeight, surface: gameSurface(), tileSize: 256, tileResolution: 129, residentTileBudget: 25, residentByteBudget: 32_000_000 });
|
|
91
132
|
*/
|
|
92
133
|
declare class TerrainTiles extends Object3D implements IComputeDriven {
|
|
@@ -107,19 +148,63 @@ declare class TerrainTiles extends Object3D implements IComputeDriven {
|
|
|
107
148
|
get residentColliderKeys(): readonly string[];
|
|
108
149
|
get lodLevelCount(): number;
|
|
109
150
|
get lodTransitions(): number;
|
|
110
|
-
/**
|
|
111
|
-
|
|
151
|
+
/**
|
|
152
|
+
* What the main pass submits for terrain levels right now: `tiles` surfaces, held as `blocks`
|
|
153
|
+
* merged super-tiles plus one mesh each for the tiles that are not merged (a lone block member or
|
|
154
|
+
* a tile mid LOD morph). `draws` is `blocks` plus those individual meshes, so with the merge off
|
|
155
|
+
* it is exactly the number of visible level meshes — the census the merge exists to cut. `rebuilds`
|
|
156
|
+
* counts the block geometries built over this residency owner's life.
|
|
157
|
+
*/
|
|
158
|
+
get terrainTiles(): {
|
|
159
|
+
readonly blocks: number;
|
|
160
|
+
readonly draws: number;
|
|
161
|
+
readonly rebuilds: number;
|
|
162
|
+
readonly tiles: number;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Resident tiles mid LOD morph right now, so `process` is still owed. A blend is
|
|
166
|
+
* `LOD_TRANSITION_FRAMES` frames long and only `process` advances it, so a caller that skips
|
|
167
|
+
* `process` while its follow point is standing still would freeze every blend on frame one.
|
|
168
|
+
*/
|
|
169
|
+
get blendingTiles(): number;
|
|
170
|
+
/**
|
|
171
|
+
* Maximum per-render-frame displacement of the visible LOD surface during transitions.
|
|
172
|
+
*
|
|
173
|
+
* `undefined` when validation is off: the pop is sampled by measuring the rendered surface twice
|
|
174
|
+
* a frame, and reporting the empty value `0` instead of a measurement would be a lie a caller
|
|
175
|
+
* could assert on.
|
|
176
|
+
*/
|
|
177
|
+
get maxLodPop(): number | undefined;
|
|
112
178
|
/** Maximum number of rendered frames during which an LOD transition remained observable. */
|
|
113
179
|
get maxLodTransitionFrames(): number;
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Maximum visible edge gap observed across follow/process calls for this residency owner, or
|
|
182
|
+
* `undefined` when validation is off — see `maxLodPop`.
|
|
183
|
+
*/
|
|
184
|
+
get maxSeamGap(): number | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Maximum remaining visible gap after skirt or bridge coverage observed across follow/process
|
|
187
|
+
* calls, or `undefined` when validation is off — see `maxLodPop`.
|
|
188
|
+
*/
|
|
189
|
+
get maxVisualSeamGap(): number | undefined;
|
|
118
190
|
/** Number of mixed-LOD edge reconciliations observed during this residency lifetime. */
|
|
119
191
|
get stitchedEdgeCount(): number;
|
|
192
|
+
/**
|
|
193
|
+
* A tile or a collider the last `follow` wanted and its budget refused, so the next one is owed
|
|
194
|
+
* work even for an unmoved follow point. `0` once a pass wanted nothing it did not get.
|
|
195
|
+
*/
|
|
196
|
+
get deferredAdmissions(): number;
|
|
120
197
|
get warmupNodes(): readonly unknown[];
|
|
121
198
|
getTile(key: string): IWorldTile | undefined;
|
|
122
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Move residency to the followed point: admit, evict and re-level every tile.
|
|
201
|
+
*
|
|
202
|
+
* `budget` caps what one call may admit. A tile the budget refuses is not resident this pass and
|
|
203
|
+
* costs nothing to try again, because `follow` recomputes the wanted set on every call — which is
|
|
204
|
+
* what makes this safe to defer: the tile was not drawn before either, so a refused admission
|
|
205
|
+
* leaves a gap rather than a hole. Omitted, a call admits everything it wants, as it always did.
|
|
206
|
+
*/
|
|
207
|
+
follow(position: IWorldTilesFollowPosition | Pick<Vector3, "x" | "z">, budget?: IAdmissionBudget): void;
|
|
123
208
|
heightAt(x: number, z: number): number;
|
|
124
209
|
normalAt(x: number, z: number, target?: Vector3): Vector3;
|
|
125
210
|
sample(channel: string, x: number, z: number): number;
|
|
@@ -130,6 +215,37 @@ declare class TerrainTiles extends Object3D implements IComputeDriven {
|
|
|
130
215
|
dispose(): void;
|
|
131
216
|
}
|
|
132
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The oracle for terrain geometry that is otherwise trusted every frame.
|
|
220
|
+
*
|
|
221
|
+
* `TerrainTiles` re-derives what it already knows — every rendered vertex checked for finiteness,
|
|
222
|
+
* every resident seam re-measured, every LOD blend's visible displacement sampled — and that is a
|
|
223
|
+
* measurement, not a mechanism: on a 289-tile ring a six-second walk paid ~270 ms in three loops
|
|
224
|
+
* that only throw when something is already broken. So it is off by default and the shipped frame
|
|
225
|
+
* trusts the cheap change detector instead, which compares buffer versions and the residency and
|
|
226
|
+
* LOD state the class writes itself.
|
|
227
|
+
*
|
|
228
|
+
* `TN_TERRAIN_VALIDATE=1`, `?tnTerrainValidate=1`, or `validate: true` on the constructor puts the
|
|
229
|
+
* loops back and every assertion with them. It is a validation mode, not a proof: it proves the
|
|
230
|
+
* frames it ran on.
|
|
231
|
+
*/
|
|
232
|
+
/** Marker printed when terrain validation is on, so a log says which mode produced it. */
|
|
233
|
+
declare const TERRAIN_VALIDATE_MARKER = "TN_TERRAIN_VALIDATE";
|
|
234
|
+
/** The launch flag. */
|
|
235
|
+
declare const TERRAIN_VALIDATE_FLAG = "TN_TERRAIN_VALIDATE";
|
|
236
|
+
/**
|
|
237
|
+
* Whether `TN_TERRAIN_VALIDATE` asks for terrain validation on this launch.
|
|
238
|
+
*
|
|
239
|
+
* @situation turn terrain's per-frame seam, LOD pop and vertex checks on for one run
|
|
240
|
+
* @situation assert the terrain geometry a game streams before it ships
|
|
241
|
+
* @constraint off by default: it is the work it checks, every frame
|
|
242
|
+
* @example // Reads its own the way `renderListValidationRequested` does: a native launch sets the
|
|
243
|
+
* // environment variable, a browser asks with the query string, a test or harness sets the
|
|
244
|
+
* // global. `0` and `false` are off, so a saved URL that enabled it still says "off".
|
|
245
|
+
* const tiles = new TerrainTiles({ ...options, validate: terrainValidationRequested() });
|
|
246
|
+
*/
|
|
247
|
+
declare function terrainValidationRequested(): boolean;
|
|
248
|
+
|
|
133
249
|
type WorldGenerationPath = "gpu" | "cpu-fallback" | "unsupported";
|
|
134
250
|
interface IWorldComputeLimits {
|
|
135
251
|
readonly maxComputeInvocationsPerWorkgroup: number;
|
|
@@ -171,6 +287,776 @@ interface IWorldCapabilitiesOptions {
|
|
|
171
287
|
*/
|
|
172
288
|
declare function getWorldCapabilities(options?: IWorldCapabilitiesOptions): IWorldCapabilities;
|
|
173
289
|
|
|
290
|
+
/**
|
|
291
|
+
* The `world.json` package contract: a versioned manifest that names a raw heightmap, stable
|
|
292
|
+
* asset ids, placement runs and the square cells that stream them.
|
|
293
|
+
*
|
|
294
|
+
* Every value is authored by the world exporter and read by the runtime, so the game never
|
|
295
|
+
* hard-codes a cell layout or an asset path. Validation never throws on malformed JSON-shaped
|
|
296
|
+
* input: it collects named errors so an exporter can report every problem in one pass.
|
|
297
|
+
*/
|
|
298
|
+
type WorldPackageErrorCode = "WORLD_VERSION_MISMATCH" | "WORLD_UNKNOWN_ASSET" | "WORLD_RUN_OUT_OF_RANGE" | "WORLD_CELL_OUTSIDE_EXTENT" | "WORLD_MALFORMED";
|
|
299
|
+
interface IWorldPackageError {
|
|
300
|
+
readonly code: WorldPackageErrorCode;
|
|
301
|
+
readonly message: string;
|
|
302
|
+
/** JSON-shaped path to the offending field, e.g. `cells[2].runs[0].offset`. */
|
|
303
|
+
readonly path: string;
|
|
304
|
+
}
|
|
305
|
+
interface IWorldExtent {
|
|
306
|
+
readonly minX: number;
|
|
307
|
+
readonly minZ: number;
|
|
308
|
+
readonly sizeX: number;
|
|
309
|
+
readonly sizeZ: number;
|
|
310
|
+
}
|
|
311
|
+
interface IWorldTerrain {
|
|
312
|
+
/** Package-relative path to the raw little-endian uint16 heightmap. */
|
|
313
|
+
readonly heightmap: string;
|
|
314
|
+
/** Vertex counts; `columns = sizeX / spacing + 1`, likewise `rows`. */
|
|
315
|
+
readonly columns: number;
|
|
316
|
+
readonly rows: number;
|
|
317
|
+
/** Metres between adjacent heightmap vertices on both axes. */
|
|
318
|
+
readonly spacing: number;
|
|
319
|
+
readonly heightMin: number;
|
|
320
|
+
readonly heightMax: number;
|
|
321
|
+
/** Optional opaque layer-mask paths, passed through to the game's surface untouched. */
|
|
322
|
+
readonly layers?: Readonly<Record<string, string>>;
|
|
323
|
+
}
|
|
324
|
+
interface IWorldAssetLod {
|
|
325
|
+
readonly glb: string;
|
|
326
|
+
readonly distance: number;
|
|
327
|
+
}
|
|
328
|
+
interface IWorldAssetBounds {
|
|
329
|
+
readonly min: readonly [number, number, number];
|
|
330
|
+
readonly max: readonly [number, number, number];
|
|
331
|
+
}
|
|
332
|
+
interface IWorldAsset {
|
|
333
|
+
readonly glb: string;
|
|
334
|
+
readonly lods?: readonly IWorldAssetLod[];
|
|
335
|
+
readonly bounds: IWorldAssetBounds;
|
|
336
|
+
readonly maxDistance?: number;
|
|
337
|
+
}
|
|
338
|
+
interface IWorldRun {
|
|
339
|
+
readonly asset: string;
|
|
340
|
+
/** Record offset into `placements`; one record is eight float32 values. */
|
|
341
|
+
readonly offset: number;
|
|
342
|
+
readonly count: number;
|
|
343
|
+
}
|
|
344
|
+
interface IWorldCell {
|
|
345
|
+
readonly x: number;
|
|
346
|
+
readonly z: number;
|
|
347
|
+
readonly runs: readonly IWorldRun[];
|
|
348
|
+
readonly chunks?: readonly string[];
|
|
349
|
+
}
|
|
350
|
+
interface IWorldPackage {
|
|
351
|
+
readonly version: 1;
|
|
352
|
+
readonly extent: IWorldExtent;
|
|
353
|
+
readonly cellSize: number;
|
|
354
|
+
readonly terrain: IWorldTerrain;
|
|
355
|
+
readonly assets: Readonly<Record<string, IWorldAsset>>;
|
|
356
|
+
readonly placements: string;
|
|
357
|
+
readonly cells: readonly IWorldCell[];
|
|
358
|
+
}
|
|
359
|
+
interface IWorldPackageValidationOptions {
|
|
360
|
+
/** Byte length of the placement buffer the runs index into. */
|
|
361
|
+
readonly placementsByteLength: number;
|
|
362
|
+
/** When known, the heightmap length must be `columns * rows * 2`. */
|
|
363
|
+
readonly heightmapByteLength?: number;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Validate a `world.json` manifest against the v1 contract.
|
|
367
|
+
*
|
|
368
|
+
* Never throws on garbage input: a non-object manifest is `WORLD_MALFORMED`. Every problem is
|
|
369
|
+
* collected, so an exporter sees the complete list at once.
|
|
370
|
+
*
|
|
371
|
+
* @situation check a Blender-exported world package before the runtime attaches anything
|
|
372
|
+
* @situation report why a world package cannot be streamed
|
|
373
|
+
* @constraint validation only checks structure and ranges; it never fetches the heightmap or GLBs
|
|
374
|
+
* @example const { ok, errors } = validateWorldPackage(json, { placementsByteLength: buffer.byteLength });
|
|
375
|
+
*/
|
|
376
|
+
declare function validateWorldPackage(manifest: unknown, options: IWorldPackageValidationOptions): {
|
|
377
|
+
ok: boolean;
|
|
378
|
+
errors: IWorldPackageError[];
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Borrow the run's placement records as a live view over the placement buffer.
|
|
382
|
+
*
|
|
383
|
+
* @situation feed one cell's instance transforms into a batch without copying
|
|
384
|
+
* @constraint the returned view aliases the caller's buffer; writing to it mutates the source
|
|
385
|
+
* @example const records = cellPlacements(buffer, { asset: "tree", offset: 0, count: 120 });
|
|
386
|
+
*/
|
|
387
|
+
declare function cellPlacements(placements: ArrayBuffer, run: IWorldRun): Float32Array;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Build a game-usable `sampleHeight` from a raw v1 heightmap.
|
|
391
|
+
*
|
|
392
|
+
* The returned function interpolates bilinearly in world units and clamps to the map edges, so it
|
|
393
|
+
* plugs straight into `Heightfield.fromSampler` and `TerrainTiles`. Height is
|
|
394
|
+
* `heightMin + v / 65535 * (heightMax - heightMin)` at vertex `(column, row)`.
|
|
395
|
+
*
|
|
396
|
+
* @situation turn an exported raw heightmap into terrain collision and rendering
|
|
397
|
+
* @situation query ground height from a Blender-authored world package
|
|
398
|
+
* @constraint the sampler reads the game's data; the framework never selects a terrain shape
|
|
399
|
+
* @example const sampleHeight = heightSamplerFromHeightmap(terrain, extent, await loadWorldHeightmap(url));
|
|
400
|
+
*/
|
|
401
|
+
declare function heightSamplerFromHeightmap(terrain: IWorldTerrain, extent: IWorldExtent, data: Uint16Array): (x: number, z: number) => number;
|
|
402
|
+
/**
|
|
403
|
+
* Fetch a raw little-endian uint16 heightmap and expose it as samples.
|
|
404
|
+
*
|
|
405
|
+
* @situation load a world package's heightmap once before building terrain
|
|
406
|
+
* @constraint a non-OK response throws; bytes are byte-swapped only on a big-endian host
|
|
407
|
+
* @example const data = await loadWorldHeightmap("/world/terrain/heightmap.u16");
|
|
408
|
+
*/
|
|
409
|
+
declare function loadWorldHeightmap(url: string): Promise<Uint16Array>;
|
|
410
|
+
|
|
411
|
+
type Channel = "r" | "g" | "b" | "a";
|
|
412
|
+
/** One texture set: albedo (and optionally a normal map) tiled in metres, tinted in linear. */
|
|
413
|
+
interface ITerrainSplatLayer {
|
|
414
|
+
readonly id: string;
|
|
415
|
+
readonly tile: number;
|
|
416
|
+
readonly tint: readonly [number, number, number];
|
|
417
|
+
readonly normal?: boolean;
|
|
418
|
+
readonly saturation?: number;
|
|
419
|
+
/** Box-projected, for cliffs a top-down projection smears. */
|
|
420
|
+
readonly triplanar?: boolean;
|
|
421
|
+
}
|
|
422
|
+
/** A layer blended over what is below it by one mask channel, remapped from `lo..hi`. */
|
|
423
|
+
interface ITerrainSplatMaskedLayer extends ITerrainSplatLayer {
|
|
424
|
+
readonly mask: string;
|
|
425
|
+
/** The component of a `rgb` mask; ignored by a single-component mask. */
|
|
426
|
+
readonly channel: "r" | "g" | "b";
|
|
427
|
+
readonly lo: number;
|
|
428
|
+
readonly hi: number;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The package's terrain table (`terrain.layers.table`). Written by the export recipe
|
|
432
|
+
* `export_terrain_layers.py` from the game's own table; every value is the game's.
|
|
433
|
+
*/
|
|
434
|
+
interface ITerrainSplatTable {
|
|
435
|
+
readonly base: ITerrainSplatLayer;
|
|
436
|
+
/** Noise (0..1 at `scale` per metre) that pushes every mask edge by +-`push`. */
|
|
437
|
+
readonly breakup: {
|
|
438
|
+
readonly scale: number;
|
|
439
|
+
readonly push: number;
|
|
440
|
+
};
|
|
441
|
+
/** Large-scale brightness variation against visible tiling, mapped to `min..max`. */
|
|
442
|
+
readonly macro: {
|
|
443
|
+
readonly scale: number;
|
|
444
|
+
readonly min: number;
|
|
445
|
+
readonly max: number;
|
|
446
|
+
};
|
|
447
|
+
readonly layers: readonly ITerrainSplatMaskedLayer[];
|
|
448
|
+
/**
|
|
449
|
+
* `terrain.layers.splat`: `planes` RGBA8 planes of `size`², rows bottom-up, covering the
|
|
450
|
+
* package extent. `masks` names each mask's plane and component (`rgb` = pick by the layer's
|
|
451
|
+
* `channel`).
|
|
452
|
+
*/
|
|
453
|
+
readonly splat: {
|
|
454
|
+
readonly size: number;
|
|
455
|
+
readonly planes: number;
|
|
456
|
+
readonly masks: Readonly<Record<string, readonly [number, Channel | "rgb"]>>;
|
|
457
|
+
};
|
|
458
|
+
/** Package-relative folder of `<id>_diff.jpg` and `<id>_nrm.jpg`. */
|
|
459
|
+
readonly textures: string;
|
|
460
|
+
}
|
|
461
|
+
interface ILoadTerrainSplatOptions {
|
|
462
|
+
readonly assets: IAssetLoader;
|
|
463
|
+
/** The world package's `world.json`, as `WorldCells.load` takes it. */
|
|
464
|
+
readonly url: string;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The splat terrain surface a world package describes, for `WorldCells.load({ surface })`.
|
|
468
|
+
*
|
|
469
|
+
* Layers blend over a base by mask channels read as linear data (the masks ship raw, beside the
|
|
470
|
+
* heightmap, so no cook moves a blend threshold), with noise-broken edges and macro brightness
|
|
471
|
+
* variation. Texture sets tile in world metres on the package's ground plane (x, -z: a Z-up
|
|
472
|
+
* authoring tool's x and y), cliffs can be triplanar, and the base plus any layer that asks carries
|
|
473
|
+
* a normal map. Nothing here is a look choice: textures, tiles, tints, thresholds and noise scales
|
|
474
|
+
* all come from the package's table, which the game authors once and its DCC shares.
|
|
475
|
+
*
|
|
476
|
+
* @situation terrain textured by splat masks exported from Blender with the world package
|
|
477
|
+
* @situation the game's terrain should match the DCC's terrain material without a second copy
|
|
478
|
+
* @constraint the package's world.json must carry `terrain.layers.table` and `terrain.layers.splat`, written by the `export_terrain_layers.py` recipe
|
|
479
|
+
* @constraint WebGPU allows 16 sampled textures per stage: planes + diffuse maps + normal maps must fit
|
|
480
|
+
* @override every value comes from the package's table; the returned material is the game's to adjust
|
|
481
|
+
* @example
|
|
482
|
+
* const surface = await loadTerrainSplat({ assets: ctx.assets, url: "world/world.json" });
|
|
483
|
+
* const world = await WorldCells.load({ assets: ctx.assets, url: "world/world.json", surface, follow, ring: 2 });
|
|
484
|
+
*/
|
|
485
|
+
declare function loadTerrainSplat(options: ILoadTerrainSplatOptions): Promise<Material>;
|
|
486
|
+
|
|
487
|
+
/** The point a streamed world follows; an `Object3D` satisfies this shape. */
|
|
488
|
+
interface IWorldCellsFollow {
|
|
489
|
+
readonly position: {
|
|
490
|
+
readonly x: number;
|
|
491
|
+
readonly z: number;
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** Hard caps on what stays resident. A cap that is reached reports pressure, it never throws. */
|
|
495
|
+
interface IWorldCellsBudget {
|
|
496
|
+
readonly residentCells: number;
|
|
497
|
+
readonly instances: number;
|
|
498
|
+
/** Placement-record bytes (`32` per instance) the resident cells are allowed to weigh. */
|
|
499
|
+
readonly bytes: number;
|
|
500
|
+
}
|
|
501
|
+
/** Terrain options forwarded to the composed `TerrainTiles`; `tileSize` defaults to `cellSize`. */
|
|
502
|
+
interface IWorldCellsTerrainOptions {
|
|
503
|
+
readonly tileSize?: number;
|
|
504
|
+
readonly tileResolution?: number;
|
|
505
|
+
readonly lodFactors?: readonly number[];
|
|
506
|
+
readonly lodDistances?: readonly number[];
|
|
507
|
+
readonly skirtDepth?: number;
|
|
508
|
+
/**
|
|
509
|
+
* Tiles kept resident around the follow tile, independent of the cell `ring`: terrain can reach
|
|
510
|
+
* to the horizon while props stay near. Defaults to `ring`, so raising only this widens the
|
|
511
|
+
* ground without widening the props, and the far tiles take the coarser `lodDistances` levels.
|
|
512
|
+
*/
|
|
513
|
+
readonly streamRadius?: number;
|
|
514
|
+
/**
|
|
515
|
+
* Tiles that get a `createCollider` body, by Chebyshev radius from the follow tile. Defaults to
|
|
516
|
+
* `ring`. A tile entering or leaving it creates or disposes its collider, so a wide terrain
|
|
517
|
+
* radius does not have to mean a physics body for every tile in it.
|
|
518
|
+
*/
|
|
519
|
+
readonly colliderRadius?: number;
|
|
520
|
+
}
|
|
521
|
+
interface IWorldCellsLoadOptions {
|
|
522
|
+
/**
|
|
523
|
+
* Logical path of `world.json` (`world/world.json`); every other path in the package resolves
|
|
524
|
+
* against it. A leading `/` is accepted and stripped, so an uncompiled `public/world` keeps
|
|
525
|
+
* working.
|
|
526
|
+
*/
|
|
527
|
+
readonly url: string;
|
|
528
|
+
/**
|
|
529
|
+
* Loader the package's paths resolve through — its manifest, or the authored names when there is
|
|
530
|
+
* none. Defaults to a fresh `createAssetLoader()`; inside a game, pass `ctx.assets` so the
|
|
531
|
+
* package's compiled output and compressed textures reach the renderer the game booted with.
|
|
532
|
+
*
|
|
533
|
+
* The world releases the source models it asks this loader for only when it created the loader
|
|
534
|
+
* itself: each asset's requested level paths are released through `release("model", path)` once
|
|
535
|
+
* the last asset naming a path is gone, so a streamed package does not pin an internally owned
|
|
536
|
+
* cache. An explicitly supplied loader is the caller's — the world never releases its cache, so a
|
|
537
|
+
* second game or world holding the same model keeps it alive; the caller ends that lifetime with
|
|
538
|
+
* its own `release`. The loader is never cleared, and a path this world never asked for is never
|
|
539
|
+
* touched either way.
|
|
540
|
+
*/
|
|
541
|
+
readonly assets?: IAssetLoader;
|
|
542
|
+
/** Game-owned terrain surface, handed straight to `TerrainTiles`. */
|
|
543
|
+
readonly surface: IWorldTilesOptions["surface"];
|
|
544
|
+
/** Passed straight to `TerrainTiles` for per-tile colliders. */
|
|
545
|
+
readonly createCollider?: IWorldTilesOptions["createCollider"];
|
|
546
|
+
/** Read once per update; an `Object3D` works. */
|
|
547
|
+
readonly follow: IWorldCellsFollow;
|
|
548
|
+
/** Chebyshev radius in cells to keep resident; a cell leaves only beyond `ring + 1`. */
|
|
549
|
+
readonly ring: number;
|
|
550
|
+
readonly budgets: IWorldCellsBudget;
|
|
551
|
+
readonly terrain?: IWorldCellsTerrainOptions;
|
|
552
|
+
/**
|
|
553
|
+
* How a scattered part whose own material is `transparent` is drawn. `"cutout"` (the default)
|
|
554
|
+
* gives the part a clone of that material with `transparent: false` and an `alphaTest`, so the
|
|
555
|
+
* instanced draw needs no per-instance sorting and the depth buffer rejects what is behind it;
|
|
556
|
+
* `"blend"` draws the material as authored. One clone per asset part, never per cell.
|
|
557
|
+
*/
|
|
558
|
+
readonly transparentScatter?: "cutout" | "blend";
|
|
559
|
+
/**
|
|
560
|
+
* Shadows for the streamed world, all off by default. `cast` makes scattered batches cast into
|
|
561
|
+
* the scene's shadow map, but only their `castLevels` finest distance levels (default 1: the
|
|
562
|
+
* near shape), since a far LOD's shadow is sub-texel in any open-world shadow window and every
|
|
563
|
+
* caster is redrawn per shadow level. `receive` lets scatter and terrain receive shadows.
|
|
564
|
+
*/
|
|
565
|
+
readonly shadows?: {
|
|
566
|
+
readonly cast?: boolean;
|
|
567
|
+
readonly castLevels?: number;
|
|
568
|
+
readonly receive?: boolean;
|
|
569
|
+
/**
|
|
570
|
+
* @deprecated Accepted and ignored. Clusters replace it: what a shadow level submits is bounded
|
|
571
|
+
* by the clusters its own window covers, not by a hand-tuned radius. It still validates, so a
|
|
572
|
+
* game that set it keeps compiling and keeps loading.
|
|
573
|
+
*/
|
|
574
|
+
readonly castDistance?: number;
|
|
575
|
+
/**
|
|
576
|
+
* Called at most once a second after streamed records changed, so the shadow levels that read
|
|
577
|
+
* them redraw. `VirtualShadowNode.invalidateAll` is the whole of it, and a call with no region
|
|
578
|
+
* is exactly that. Not called per frame, and not called at all when nothing changed.
|
|
579
|
+
*
|
|
580
|
+
* When it is called about records, it is called with the union of the bounds of the records
|
|
581
|
+
* that changed — the placement position widened by the asset's own bounds, unioned over the
|
|
582
|
+
* records of this update and nothing else, so a hook that forwards it to
|
|
583
|
+
* `VirtualShadowNode.invalidateRegion` redraws the levels whose window covers them and leaves
|
|
584
|
+
* the rest of the cascade holding its maps. A call with no region is still a blanket one: the
|
|
585
|
+
* prewarm passes it while a caster's first draw is owed, and there is no region for that.
|
|
586
|
+
*/
|
|
587
|
+
readonly invalidate?: (region?: IShadowRegion) => void;
|
|
588
|
+
/**
|
|
589
|
+
* An asset whose authored bounds are shorter than this casts into the finest shadow level only,
|
|
590
|
+
* default 1.5 m. Ground cover below it — ferns, grass, bushes — casts a shadow that is sub-texel
|
|
591
|
+
* noise at every range a wide level covers, and costs one draw per key per level there. Raise it
|
|
592
|
+
* to draw everything at every range, lower it to cull more. Measured on the authored bounds, so
|
|
593
|
+
* a package that scales a fern up four times is judged on the un-scaled fern.
|
|
594
|
+
*/
|
|
595
|
+
readonly smallCasterMetres?: number;
|
|
596
|
+
};
|
|
597
|
+
/**
|
|
598
|
+
* Triangles a hand-placed chunk's merged material group may reach before one of its
|
|
599
|
+
* `InstancedMesh` is left instanced. Defaults to 43,690, the same 4 MiB
|
|
600
|
+
* budget the merged buffers are split on counted in the worst case, a de-indexed triangle: a merged
|
|
601
|
+
* group is a single draw, so expanding into it wins until the merged geometry is too big for the
|
|
602
|
+
* frame, and above it the instanced draw is the cheaper of the two. A group that crosses the budget
|
|
603
|
+
* is split into several meshes in traversal order rather than grown, and an instanced shape over
|
|
604
|
+
* 2,048 triangles is never expanded. Everything else in a chunk merges regardless — one mesh per
|
|
605
|
+
* material, in the main pass and in every shadow pass that redraws it.
|
|
606
|
+
*/
|
|
607
|
+
readonly chunkMergeMaxTriangles?: number;
|
|
608
|
+
/**
|
|
609
|
+
* `(url) => Promise<Object3D>`, overriding `assets.model`; a raw `GLTFLoader` or a game's own
|
|
610
|
+
* loader works. The url is the authored one, so a compiled package wants `assets` instead.
|
|
611
|
+
*/
|
|
612
|
+
readonly loadModel?: (url: string) => Promise<Object3D>;
|
|
613
|
+
/**
|
|
614
|
+
* Model loads in flight at once, assets and chunks together. Defaults to `loadAll`'s
|
|
615
|
+
* `concurrency`, six.
|
|
616
|
+
*/
|
|
617
|
+
readonly concurrency?: number;
|
|
618
|
+
/**
|
|
619
|
+
* Cell-asset batches refiltered per `update`, nearest cell first. Defaults to 16. A cell that did
|
|
620
|
+
* not get its turn keeps drawing the batches it has until a later update replaces them.
|
|
621
|
+
*/
|
|
622
|
+
readonly rebuildsPerUpdate?: number;
|
|
623
|
+
/**
|
|
624
|
+
* Seconds of motion to stream ahead of, default 1.5: the ring centres on where the follow point
|
|
625
|
+
* will be (`position + velocity * prefetchSeconds`), so a fast camera finds cells already loaded
|
|
626
|
+
* instead of outrunning them. The lead is capped at three quarters of the ring, so the cell the
|
|
627
|
+
* follow point is in always stays resident. `0` streams around the follow point itself.
|
|
628
|
+
*/
|
|
629
|
+
readonly prefetchSeconds?: number;
|
|
630
|
+
/**
|
|
631
|
+
* New batch meshes created per update, default 2. On WebGPU each new InstancedMesh builds its
|
|
632
|
+
* own shader the first frame it draws (~10-20 ms of main thread), so a burst of first-seen assets
|
|
633
|
+
* is spread over frames instead of stacked into one; recycled meshes are not counted.
|
|
634
|
+
*/
|
|
635
|
+
readonly freshMeshesPerUpdate?: number;
|
|
636
|
+
/**
|
|
637
|
+
* Side of the world-grid square one shared batch's records are clustered into, in world units.
|
|
638
|
+
* Defaults to the package's own `cellSize`, which is the square the placements are already cut
|
|
639
|
+
* on. Each `(key, cluster)` is its own caster InstancedMesh with its own bounds on the shadow
|
|
640
|
+
* caster layer, so a virtual-shadow level submits only the clusters its window covers. The main
|
|
641
|
+
* pass keeps one mesh per `asset:level:part` whatever this is, so it costs main-pass draws
|
|
642
|
+
* nothing. A larger square means fewer caster meshes and more of the world in every level render.
|
|
643
|
+
*/
|
|
644
|
+
readonly clusterSize?: number;
|
|
645
|
+
/**
|
|
646
|
+
* Milliseconds one `update` may spend admitting streamed content — cell batches, terrain tiles,
|
|
647
|
+
* colliders and `maxDistance`/`lods` refilters together. Defaults to 2, and `Infinity` opts out
|
|
648
|
+
* of the ceiling entirely.
|
|
649
|
+
*
|
|
650
|
+
* The ceiling is a budget, not a promise: one unit of work always finishes, so a frame spends at
|
|
651
|
+
* most this plus its last unit. Work that does not fit waits for the next `update` rather than
|
|
652
|
+
* being dropped, and `stats().admission` reports what is waiting. A frame that admits nothing
|
|
653
|
+
* costs nothing here.
|
|
654
|
+
*/
|
|
655
|
+
readonly admissionBudgetMs?: number;
|
|
656
|
+
/**
|
|
657
|
+
* Milliseconds source for the admission budget, `performance.now` by default. Injectable so a
|
|
658
|
+
* test can prove the ceiling instead of hoping a machine is slow enough to show it.
|
|
659
|
+
*/
|
|
660
|
+
readonly admissionNow?: () => number;
|
|
661
|
+
/**
|
|
662
|
+
* The screen-space projection the extra levels of a baked AutoLOD chain are switched at, for an
|
|
663
|
+
* asset whose `world.json` entry names no `lods` of its own.
|
|
664
|
+
*
|
|
665
|
+
* A chain measures its levels' geometric error in world units and the model runtime turns that
|
|
666
|
+
* into pixels per frame; a batched draw cannot, so the same test is solved for distance once,
|
|
667
|
+
* here, and every placement beyond it draws the next level. `fovY` is the vertical field of view
|
|
668
|
+
* in degrees and `viewportHeight` the drawing-buffer height in raster pixels — a CSS height is the
|
|
669
|
+
* wrong number, exactly as it is for the model runtime.
|
|
670
|
+
*
|
|
671
|
+
* Defaults are 60° over 1080 raster rows: a desktop view, and what the switch distance is a
|
|
672
|
+
* function of. A world whose camera this class cannot see should pass its own, or a world with a
|
|
673
|
+
* narrow field of view (a telephoto gun sight) or a tall window will switch earlier than its
|
|
674
|
+
* pixels ask for. An asset with authored `lods` ignores all three numbers entirely: the package is
|
|
675
|
+
* the authority on its own shape.
|
|
676
|
+
*
|
|
677
|
+
* `maxPixelError` is the error budget, in pixels, every synthesized level is selected against, and
|
|
678
|
+
* it defaults to 4 rather than to the budget the asset's chain was registered with. The
|
|
679
|
+
* registered budget is calibrated for a `ModelLod`, which picks a level for *one* mesh from where
|
|
680
|
+
* that mesh is on screen; an instanced draw cannot, because every placement in the draw is at its
|
|
681
|
+
* own distance and the draw is one call with one geometry. The batched answer has to be solved
|
|
682
|
+
* once, for the whole key, which moves every switch distance outward by the ratio of the budgets —
|
|
683
|
+
* and a 1 px budget is calibrated for a hero object filling the screen. At 1 px a tree's switches
|
|
684
|
+
* land at 400–600 m on a 1080-row view, which on a 2 km map with a 640 m ring is past most of the
|
|
685
|
+
* resident world: the levels are all there and none of them is ever selected, which is the same
|
|
686
|
+
* full triangle bill the chain was added to remove. Four pixels puts the first switch inside the
|
|
687
|
+
* ring, where it can actually do something.
|
|
688
|
+
*/
|
|
689
|
+
readonly autoLod?: {
|
|
690
|
+
readonly fovY?: number;
|
|
691
|
+
readonly viewportHeight?: number;
|
|
692
|
+
readonly maxPixelError?: number;
|
|
693
|
+
};
|
|
694
|
+
/**
|
|
695
|
+
* The GPU-driven main pass: one compute dispatch culls and LOD-selects every resident placement
|
|
696
|
+
* into a shared matrix buffer, and each main key draws its own region of it through an indirect
|
|
697
|
+
* record. The CPU keeps only the coarse per-cell visibility, so a walking camera costs no repack
|
|
698
|
+
* and no `maxDistance`/`lods` refilter.
|
|
699
|
+
*
|
|
700
|
+
* `true` by default where the backend can run it: it needs compute, storage buffers and
|
|
701
|
+
* `drawIndexedIndirect`, and a backend without them falls back to exactly this class's CPU path —
|
|
702
|
+
* a lost saving, never a wrong picture. `gpuScene: false`, `TN_GPU_SCENE=0` or `?tnGpuScene=0`
|
|
703
|
+
* turns it off, and `stats().gpuScene` and the `TN_WORLD_GPU_SCENE` line say which path a run
|
|
704
|
+
* took.
|
|
705
|
+
*/
|
|
706
|
+
readonly gpuScene?: boolean;
|
|
707
|
+
/**
|
|
708
|
+
* Hold every dispatch's indirect args against the pure `cullAndSelect` reference and print the keys
|
|
709
|
+
* that disagree. `false` by default, and `TN_GPU_SCENE_VALIDATE=1` or `?tnGpuSceneValidate=1` turns
|
|
710
|
+
* it on.
|
|
711
|
+
*
|
|
712
|
+
* A readback is a queue submission and a mapped buffer, so this is a mode for answering "which key
|
|
713
|
+
* draws fewer instances than the CPU path" and never a walk to be measured with.
|
|
714
|
+
*/
|
|
715
|
+
readonly gpuSceneValidate?: boolean;
|
|
716
|
+
/**
|
|
717
|
+
* Sample what the GPU actually selects in the main pass — a readback of the indirect args every
|
|
718
|
+
* 30 dispatches — and report it as `stats().gpuScene` and on the `TN_WORLD_GPU_SCENE`
|
|
719
|
+
* line. Off unless asked: the engine asks when the frame budget is on, and a validation turns it on
|
|
720
|
+
* by itself, so `TN_FRAME_BUDGET`'s `mainGpuTriangles` is the GPU-selected count and not the mesh
|
|
721
|
+
* capacity `tri=` reports. Set it explicitly when driving the world without a frame budget.
|
|
722
|
+
*/
|
|
723
|
+
readonly gpuSceneTally?: boolean;
|
|
724
|
+
/**
|
|
725
|
+
* Record every GPU-dressed main batch mesh into one `BundleGroup` and replay the bundle instead of
|
|
726
|
+
* re-walking three's per-object path for each draw. Off by default: measured on machinefall's
|
|
727
|
+
* map-walk, bundles gave no CPU p50/p95 gain (the main thread is mostly idle and the frame is
|
|
728
|
+
* GPU/present bound), and `?tnBundles=0` stays the off path while `bundles: true` or
|
|
729
|
+
* `?tnBundles=1`/`TN_BUNDLES=1` turns it on. `stats().bundle` and the `TN_WORLD_BUNDLE` line say
|
|
730
|
+
* which way a run took.
|
|
731
|
+
*
|
|
732
|
+
* The render list inside a bundle is fixed when it is recorded, so a bundled mesh is never hidden:
|
|
733
|
+
* the dispatch draws zero instances for a key the camera cannot see, and an indirect draw of zero
|
|
734
|
+
* instances costs the GPU nothing. Toggling `visible` would force a re-record instead, and the
|
|
735
|
+
* shadow node's texel gate skips a bundled mesh for the same reason.
|
|
736
|
+
*/
|
|
737
|
+
readonly bundles?: boolean;
|
|
738
|
+
/**
|
|
739
|
+
* Raise the LOD distance bias when the main pass is over its share of the frame's GPU budget, and
|
|
740
|
+
* decay it back toward 1 when it is comfortably under, default true.
|
|
741
|
+
*
|
|
742
|
+
* The main pass is triangle-bound: a flyover's near and mid tree levels are millions of triangles
|
|
743
|
+
* and the GPU time tracks that count. When `renderer.gpuMainMs()` — the smoothed main-pass GPU
|
|
744
|
+
* time — exceeds `mainGpuShare` of a 60 fps frame, every LOD switch is crossed earlier by
|
|
745
|
+
* multiplying the camera distance both selection paths compare by one shared multiplier, coarsening
|
|
746
|
+
* the same placements the GPU is already struggling to draw. It rises in bounded steps, never in a
|
|
747
|
+
* jump, so no level pops. `false` pins the bias at 1 for the whole run, which is byte-identical to
|
|
748
|
+
* the selection before this feature existed: `?tnAdaptiveLod=0`, `TN_ADAPTIVE_LOD=0` or
|
|
749
|
+
* `__tnAdaptiveLod = 0` also turns it off.
|
|
750
|
+
*/
|
|
751
|
+
readonly adaptiveLod?: boolean;
|
|
752
|
+
/**
|
|
753
|
+
* The fraction of a frame's GPU time the main pass may take before the adaptive LOD bias rises,
|
|
754
|
+
* `(0, 1]`, default 0.5.
|
|
755
|
+
*
|
|
756
|
+
* The budget is `mainGpuShare x min(deltaTime, 1/60 s)`: the frame cap keeps the budget honest the
|
|
757
|
+
* same way the shadow refresh gate does, because the frame that draws an expensive main pass is
|
|
758
|
+
* itself long and reading its own inflated delta would let the cost it judges pass its own test.
|
|
759
|
+
* `?tnMainGpuShare=0.1`, `TN_MAIN_GPU_SHARE=0.1` or `__tnMainGpuShare = 0.1` forces adaptation on a
|
|
760
|
+
* GPU that would otherwise never exceed the share.
|
|
761
|
+
*/
|
|
762
|
+
readonly mainGpuShare?: number;
|
|
763
|
+
/**
|
|
764
|
+
* Bake a whole-asset octahedral impostor for every alpha-cutout foliage asset and append it as the
|
|
765
|
+
* asset's terminal, two-triangle LOD, default false.
|
|
766
|
+
*
|
|
767
|
+
* Opt-in until the impostor casts a forest's shadow: it replaces the coarsest level the wide
|
|
768
|
+
* shadow levels draw, and its two-triangle card leaves the road and forest floor almost unshaded
|
|
769
|
+
* (PR #375 visual regression), so a default world keeps its source levels as shadow casters.
|
|
770
|
+
*
|
|
771
|
+
* The bake runs inside the render cadence — one of its sixteen views per `process` — so it costs a
|
|
772
|
+
* slice of a frame rather than a hitch, and it starts only after the asset's own model has been
|
|
773
|
+
* adopted. A seamless switch needs the impostor's atlas to cover the same cutout the source draws,
|
|
774
|
+
* so an asset whose levels carry no alpha-cutout part is never baked. `false` drops only the atlas
|
|
775
|
+
* terminal: every level the authored package and its baked chain give is kept, and the authored
|
|
776
|
+
* middle-level cutout coverage a level would otherwise be missing its foliage at is still applied.
|
|
777
|
+
*/
|
|
778
|
+
readonly impostors?: boolean;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* What the GPU actually selected in the main pass, from the scene's last landed tally.
|
|
782
|
+
*
|
|
783
|
+
* `triangles` is the GPU-selected count (`instanceCount x indexCount / 3` summed over the indirect
|
|
784
|
+
* records), never the mesh-capacity upper bound `renderer.info.render.triangles` reports, and it
|
|
785
|
+
* covers the main pass only: shadow passes are not tallied. `ageFrames` is how many dispatches have
|
|
786
|
+
* passed since those bytes were issued, so a caller never mistakes a stale sample for this frame's.
|
|
787
|
+
*/
|
|
788
|
+
interface IWorldCellsGpuTally {
|
|
789
|
+
readonly instances: number;
|
|
790
|
+
readonly triangles: number;
|
|
791
|
+
readonly ageFrames: number;
|
|
792
|
+
}
|
|
793
|
+
interface IWorldCellsStats {
|
|
794
|
+
readonly residentCells: number;
|
|
795
|
+
readonly residentKeys: readonly string[];
|
|
796
|
+
/** Placement instances the resident cells hold, before any `maxDistance` filter. */
|
|
797
|
+
readonly instances: number;
|
|
798
|
+
readonly loadsInFlight: number;
|
|
799
|
+
/** Model loads waiting for a free lane; served nearest-cell first, in admission order. */
|
|
800
|
+
readonly loadsQueued: number;
|
|
801
|
+
/**
|
|
802
|
+
* Shared batches still to be minted, plus the ones minted and not yet drawn; `0` once
|
|
803
|
+
* `prewarmed` has resolved.
|
|
804
|
+
*/
|
|
805
|
+
readonly pendingPrewarm: number;
|
|
806
|
+
/** Shared batches minted by the prewarm so far, cumulative. */
|
|
807
|
+
readonly prewarmMinted: number;
|
|
808
|
+
readonly evictions: number;
|
|
809
|
+
/** Cumulative `maxDistance`/`lods` refilters performed, across every update. */
|
|
810
|
+
readonly rebuilds: number;
|
|
811
|
+
/**
|
|
812
|
+
* Cumulative refilter *passes* run, whether or not they queued a rebuild. The pass walks every
|
|
813
|
+
* resident cell, derives both distance brackets and sorts what it found, so this is what says
|
|
814
|
+
* whether a follow point standing inside the 2 m step paid for it; `rebuilds` only says whether it
|
|
815
|
+
* was worth anything. See `LEVEL_REFILTER_STEP_METRES`.
|
|
816
|
+
*/
|
|
817
|
+
readonly refilters: number;
|
|
818
|
+
/**
|
|
819
|
+
* Cumulative stale cell-assets the refilter passes found, across every update. The pass walks every
|
|
820
|
+
* resident cell and gates it as a whole before it looks at one batch, so a follow point that has
|
|
821
|
+
* crossed nothing leaves this still — which is what says the walk cost brackets and comparisons and
|
|
822
|
+
* not allocations.
|
|
823
|
+
*/
|
|
824
|
+
readonly refilterEntries: number;
|
|
825
|
+
/**
|
|
826
|
+
* Cumulative refilters whose rebuild held the same records and were settled without a write,
|
|
827
|
+
* clear or compaction. A rising share of `rebuilds` is a follow point crossing a cell's distance
|
|
828
|
+
* bracket rather than any placement crossing a gate.
|
|
829
|
+
*/
|
|
830
|
+
readonly unchanged: number;
|
|
831
|
+
readonly failures: number;
|
|
832
|
+
/**
|
|
833
|
+
* What the main-pass cull cost, cumulative, and the evidence that a settled camera costs nothing.
|
|
834
|
+
*
|
|
835
|
+
* `frustums` is one a frame that had a perspective camera — it used to be one per main mesh, ~230
|
|
836
|
+
* of them on a 2 km walk. `windows` is how many of those meshes had to re-derive their window at
|
|
837
|
+
* all, and `repacks` how many copied records: both are 0 on a frame where the visible squares did
|
|
838
|
+
* not change and no record joined or left, which is what the two integer comparisons in
|
|
839
|
+
* `SharedBatch.cullFrom` buy.
|
|
840
|
+
*/
|
|
841
|
+
readonly mainCull: {
|
|
842
|
+
readonly frustums: number;
|
|
843
|
+
readonly windows: number;
|
|
844
|
+
readonly repacks: number;
|
|
845
|
+
readonly visibleEpoch: number;
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* What the GPU-driven main pass is doing, and why it is not: `on` is the answer, `reason` is the
|
|
849
|
+
* one line `TN_WORLD_GPU_SCENE` printed, and `dispatches` is what the walk actually paid for its
|
|
850
|
+
* per-instance work. `mainCull.repacks` and `refilters` stay at `0` while it is on — that is the
|
|
851
|
+
* CPU work it replaced, counted by the same counters that report it when it is off. `dressed` over
|
|
852
|
+
* `meshes` is the marker line's own pair, and it is the one that says the scene has meshes to draw
|
|
853
|
+
* with: an `on` with `0` dressed is a ring the CPU path is still drawing.
|
|
854
|
+
*/
|
|
855
|
+
readonly gpuScene: {
|
|
856
|
+
readonly on: boolean;
|
|
857
|
+
readonly reason: string;
|
|
858
|
+
readonly dispatches: number;
|
|
859
|
+
/** Resident source records: one per placement, shared by every part of the level it reached. */
|
|
860
|
+
readonly instances: number;
|
|
861
|
+
readonly keys: number;
|
|
862
|
+
readonly dressed: number;
|
|
863
|
+
readonly meshes: number;
|
|
864
|
+
/**
|
|
865
|
+
* Instances and triangles the GPU actually selected in the main pass, and the age of that
|
|
866
|
+
* sample in dispatches. Absent until the first tally lands, never zero: the scene's own
|
|
867
|
+
* `instances` above is what it holds, and `gpuInstances` is what the GPU drew.
|
|
868
|
+
*/
|
|
869
|
+
readonly gpuInstances?: number;
|
|
870
|
+
readonly gpuTriangles?: number;
|
|
871
|
+
readonly gpuTallyAgeFrames?: number;
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* What the main pass's draw bundles are doing: `children` is how many GPU-dressed meshes are
|
|
875
|
+
* parented under the one `BundleGroup`, and `records` is how many times that group has been
|
|
876
|
+
* re-recorded since the world loaded.
|
|
877
|
+
*
|
|
878
|
+
* A record is a structural change and nothing else — a key minted or retired, a geometry or
|
|
879
|
+
* material swapped, or a GPU-scene buffer regrow that re-dresses its meshes. Streaming, culling and
|
|
880
|
+
* LOD do not move it, which is the whole claim: a 200-frame walk holds `records` at the number of
|
|
881
|
+
* keys that came and went, and a settled camera holds it still.
|
|
882
|
+
*/
|
|
883
|
+
readonly bundle: {
|
|
884
|
+
readonly on: boolean;
|
|
885
|
+
readonly children: number;
|
|
886
|
+
readonly records: number;
|
|
887
|
+
};
|
|
888
|
+
/**
|
|
889
|
+
* The runtime impostor path: how many assets appended a terminal level, how many bakes are still
|
|
890
|
+
* queued or in flight, the triangles all terminal levels submit together — the number the whole
|
|
891
|
+
* feature exists to reduce — and the cache size: the logical colour atlas bytes held (each atlas's
|
|
892
|
+
* RGBA8 colour and normal mip chains), not every physical GPU buffer a render target may own, and
|
|
893
|
+
* the budget those bytes are enforced against.
|
|
894
|
+
*/
|
|
895
|
+
readonly impostor: {
|
|
896
|
+
readonly assets: number;
|
|
897
|
+
readonly pending: number;
|
|
898
|
+
readonly atlasBytes: number;
|
|
899
|
+
readonly budgetBytes: number;
|
|
900
|
+
readonly terminalTriangles: number;
|
|
901
|
+
/**
|
|
902
|
+
* The whole-map far aggregates: one `InstancedMesh` per atlas cache key, the ORIGINAL placements
|
|
903
|
+
* they hold (`instances`), how many of those the near ring has taken back (`nearOwned`), the
|
|
904
|
+
* physical instance bytes, and how many times an aggregate's buffer has been written. Missing
|
|
905
|
+
* measurements stay absent; a settled camera holds `uploads` still.
|
|
906
|
+
*/
|
|
907
|
+
readonly far: {
|
|
908
|
+
readonly aggregates: number;
|
|
909
|
+
readonly instances: number;
|
|
910
|
+
readonly live: number;
|
|
911
|
+
readonly nearOwned: number;
|
|
912
|
+
readonly bytes: number;
|
|
913
|
+
readonly uploads: number;
|
|
914
|
+
};
|
|
915
|
+
};
|
|
916
|
+
/** Cumulative rejected requests: cells skipped, instances or bytes refused, terrain retries. */
|
|
917
|
+
readonly pressure: {
|
|
918
|
+
readonly cells: number;
|
|
919
|
+
readonly instances: number;
|
|
920
|
+
readonly bytes: number;
|
|
921
|
+
};
|
|
922
|
+
/**
|
|
923
|
+
* What the last `update` spent on admitting streamed content, and what it left behind.
|
|
924
|
+
*
|
|
925
|
+
* A rising `backlog` is the honest signal that admission is not keeping up with the follow point;
|
|
926
|
+
* it is the one number that says a player is outrunning the world.
|
|
927
|
+
*/
|
|
928
|
+
readonly admission: {
|
|
929
|
+
/** Milliseconds the last `update` spent admitting; at most the budget plus its last unit. */
|
|
930
|
+
readonly spentMs: number;
|
|
931
|
+
/** Cell-asset builds the last `update` had no budget for, still queued for a later one. */
|
|
932
|
+
readonly deferred: number;
|
|
933
|
+
/** Units of work those builds still owe — placement slices left plus meshes left to publish. */
|
|
934
|
+
readonly backlog: number;
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* A plain `{ min, max }` region, the shape a shadow level's invalidation reads and the shape a game
|
|
939
|
+
* can be handed without importing three. Plain numbers, so the object a caller records its bounds
|
|
940
|
+
* in is the object that crosses the boundary.
|
|
941
|
+
*/
|
|
942
|
+
interface IShadowRegion {
|
|
943
|
+
min: {
|
|
944
|
+
x: number;
|
|
945
|
+
y: number;
|
|
946
|
+
z: number;
|
|
947
|
+
};
|
|
948
|
+
max: {
|
|
949
|
+
x: number;
|
|
950
|
+
y: number;
|
|
951
|
+
z: number;
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Stream a Blender-authored world package by cell and keep it resident around a followed point.
|
|
956
|
+
*
|
|
957
|
+
* The class composes `TerrainTiles` for the package's heightmap, builds one `InstancedBatch` per
|
|
958
|
+
* resident cell asset run, distance level and mesh part, and loads hand-placed chunk GLBs through
|
|
959
|
+
* `loadAll` + `addInSlices`. Ring residency, per-asset `maxDistance` filtering, the per-asset `lods`
|
|
960
|
+
* levels, hard budgets and generation-tokened cancellation all live here; every geometry, material
|
|
961
|
+
* and surface still comes from the package's GLBs and the game.
|
|
962
|
+
*
|
|
963
|
+
* An asset is drawn per part, not per model: a GLB with several primitives is one `InstancedBatch`
|
|
964
|
+
* each, and a scattered part whose own material is transparent draws as an alpha cutout unless the
|
|
965
|
+
* game asks for blending, because an `InstancedMesh` cannot sort its instances.
|
|
966
|
+
*
|
|
967
|
+
* An asset whose package entry names no `lods` is drawn at the levels its own model carries a baked
|
|
968
|
+
* AutoLOD chain for: the levels an instanced draw cannot reach by itself, switched at the distance
|
|
969
|
+
* their error projects over the `autoLod` viewport. Authored `lods` win; a chain is only a fallback.
|
|
970
|
+
*
|
|
971
|
+
* @situation stream a large Blender-authored world by cell instead of one huge GLB
|
|
972
|
+
* @situation keep scattered props and hand-placed chunks resident around a moving player
|
|
973
|
+
* @situation honour per-asset draw distances and hard streaming budgets without a mid-frame throw
|
|
974
|
+
* @constraint surface is the game's; this class creates no material, colour or geometry
|
|
975
|
+
* @constraint budgets are hard caps that report pressure instead of over-committing
|
|
976
|
+
* @constraint model loads are bounded by `concurrency` (default 12) across every resident cell, not per cell
|
|
977
|
+
* @constraint refilters are bounded by `rebuildsPerUpdate` (default 16) per update, nearest cell first
|
|
978
|
+
* @constraint admission is bounded by `admissionBudgetMs` (default 2) per update across every path, plus at most one unit each for terrain and props; while props are queued terrain takes at most half, so neither starves the other, and a deferred cell keeps drawing what it has
|
|
979
|
+
* @constraint SkinnedMesh parts are skipped; an instanced copy would draw one rest pose
|
|
980
|
+
* @constraint a baked chain's switch distances are measured against `autoLod` (default 4 px of error over 60° and 1080 raster rows), because an instanced draw cannot select a level per instance; an asset with authored `lods` never consults it
|
|
981
|
+
* @override ring, budgets, terrain tile size/resolution, terrain stream and collider radius, `transparentScatter`, `clusterSize` and `shadows.invalidate`, load `concurrency`, `rebuildsPerUpdate`, `admissionBudgetMs` and the package's per-asset maxDistance
|
|
982
|
+
* @constraint `prewarmed` resolves once every prewarmed shared batch has been drawn; a game with a loading screen waits on it, and `stats().pendingPrewarm` is the same gate as a number
|
|
983
|
+
* @constraint every `asset:level:part` is one InstancedMesh for the main pass, plus one caster InstancedMesh per world-grid square of `clusterSize` on the shadow caster layer, so the main pass draws one mesh per key and a shadow level submits only the squares it covers
|
|
984
|
+
* @constraint two definitions the asset loader resolves to one model — the same cooked `glb`, the same `lods` at the same distances, the same `maxDistance` and bounds — are one asset under the lexicographically smallest id: one model load, one set of `asset:level:part` keys, one prewarm and one refcount, released when the last cell holding any member of the group leaves the ring; `TN_WORLD_ASSET_ALIAS` reports how many of the package's assets are really distinct
|
|
985
|
+
* @constraint the main pass mesh draws only the squares the render camera's frustum covers — on by default, narrowed once per frame for every main batch by the engine's render-cadence dispatch, never for an orthographic camera — a batch with nothing to draw is hidden rather than submitted at `count 0`, and `TN_WORLD_MAIN_CULL` reports both every five seconds
|
|
986
|
+
* @constraint a loaded chunk is merged by material before it is added, so it submits one draw per material rather than one per node; a skinned, multi-material or morph-target mesh, one carrying a baked AutoLOD chain, and an instanced mesh past `chunkMergeMaxTriangles` (default 43,690 triangles) or with a shape over 2,048 triangles, all keep their own geometry; a material group crossing 131,072 vertices (4 MiB of position + normal + uv) is split into several meshes in traversal order instead of one giant upload, indexed parts keep their index, and `TN_WORLD_CHUNK_MERGE` reports what the merge did and the bytes it left
|
|
987
|
+
* @constraint `shadows.castDistance` is accepted and ignored (clusters replaced it); `shadows.invalidate` is called at most once a second after streamed records changed
|
|
988
|
+
* @example
|
|
989
|
+
* const world = await WorldCells.load({ url: "/world/world.json", surface, follow, ring: 1, budgets: { residentCells: 25, instances: 20000, bytes: 8000000 } });
|
|
990
|
+
* scene.add(world);
|
|
991
|
+
* world.update();
|
|
992
|
+
*/
|
|
993
|
+
declare class WorldCells extends Group implements IComputeDriven {
|
|
994
|
+
#private;
|
|
995
|
+
readonly processCadence: "render";
|
|
996
|
+
private constructor();
|
|
997
|
+
static load(options: IWorldCellsLoadOptions): Promise<WorldCells>;
|
|
998
|
+
get released(): boolean;
|
|
999
|
+
get warmupNodes(): readonly unknown[];
|
|
1000
|
+
/**
|
|
1001
|
+
* Resolves once every minted batch has been drawn at least once, main and shadow context.
|
|
1002
|
+
*
|
|
1003
|
+
* The loading gate a game with a loading screen waits on, so the shader builds the prewarm exists
|
|
1004
|
+
* to move happen behind it instead of in the first seconds of play. It counts updates the world
|
|
1005
|
+
* was in a scene for, because a batch added while the world is not in a scene is projected by
|
|
1006
|
+
* nothing and builds nothing. `dispose` settles it too: a world torn down mid-load must not leave
|
|
1007
|
+
* a game awaiting it forever.
|
|
1008
|
+
*/
|
|
1009
|
+
get prewarmed(): Promise<void>;
|
|
1010
|
+
/**
|
|
1011
|
+
* Per-frame residency step; call it wherever `TerrainTiles.process` is called.
|
|
1012
|
+
*
|
|
1013
|
+
* Reads the follow target, keeps the in-ring cells, evicts cells beyond the hysteresis ring, and
|
|
1014
|
+
* spends one admission budget on everything the ring newly wants: cell-asset batches, terrain
|
|
1015
|
+
* tiles, colliders and the `maxDistance`/`lods` refilters the follow point has moved far enough to
|
|
1016
|
+
* have changed. Whatever does not fit waits for the next call — nothing is dropped, and
|
|
1017
|
+
* `stats().admission` is the honest report of what is waiting. A terrain budget throw is caught
|
|
1018
|
+
* and counted, and so is a teardown that throws while releasing what left; `failures` carries
|
|
1019
|
+
* both. Every other error, the game's included, escapes.
|
|
1020
|
+
*
|
|
1021
|
+
* The residency pass itself is skipped while the follow point has moved less than half a metre
|
|
1022
|
+
* since the last one and nothing is pending (a load, a cell build, a deferred terrain admission, a
|
|
1023
|
+
* prewarm mint or a shadow level not yet told), because the pass would re-derive the set the
|
|
1024
|
+
* world already holds. The drain, the prewarm, the invalidation and the main cull below it run
|
|
1025
|
+
* every time.
|
|
1026
|
+
*
|
|
1027
|
+
* `camera` is the frame's render camera, which the engine's render-cadence dispatch hands over
|
|
1028
|
+
* because it is the one this world draws with. A game that drives this world itself can pass it,
|
|
1029
|
+
* and a game that does not keeps every resident record drawn.
|
|
1030
|
+
*/
|
|
1031
|
+
update(renderer?: IRendererLike, camera?: Camera): void;
|
|
1032
|
+
/**
|
|
1033
|
+
* The render-cadence dispatch. The engine hands the frame's render camera over, and this world's
|
|
1034
|
+
* main windows follow it; see {@link #cullMainPass} for why the decision is not the meshes' own.
|
|
1035
|
+
*/
|
|
1036
|
+
process(renderer?: IRendererLike, camera?: Camera): void;
|
|
1037
|
+
attachRenderer(renderer: IRendererLike): void;
|
|
1038
|
+
/** Current per-asset reference count; an asset absent from the map is `0`. */
|
|
1039
|
+
assetRefCounts(): Readonly<Record<string, number>>;
|
|
1040
|
+
/**
|
|
1041
|
+
* Turn on the GPU-selected tally for the rest of the world's life.
|
|
1042
|
+
*
|
|
1043
|
+
* The engine calls this when the frame budget is on, and a `gpuSceneValidate` turns it on without
|
|
1044
|
+
* it. Set `gpuSceneTally: true` at load instead when a world is driven outside a frame budget.
|
|
1045
|
+
*/
|
|
1046
|
+
enableGpuSceneTally(): void;
|
|
1047
|
+
/**
|
|
1048
|
+
* The GPU's own main-pass selection from the last landed tally, or `undefined` before one lands.
|
|
1049
|
+
*
|
|
1050
|
+
* `triangles` is the GPU-selected count, which `renderer.info.render.triangles` cannot give for an
|
|
1051
|
+
* indirect draw: three counts the mesh capacity there, an upper bound over what the kernel could
|
|
1052
|
+
* select. This is what the kernel selected. Main pass only, and `ageFrames` is its staleness.
|
|
1053
|
+
*/
|
|
1054
|
+
gpuSceneTally(): IWorldCellsGpuTally | undefined;
|
|
1055
|
+
stats(): IWorldCellsStats;
|
|
1056
|
+
detach(): void;
|
|
1057
|
+
dispose(): void;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
174
1060
|
interface IHeightfieldOrigin {
|
|
175
1061
|
readonly x: number;
|
|
176
1062
|
readonly z: number;
|
|
@@ -251,4 +1137,4 @@ declare class Heightfield extends Group implements IComputeDriven {
|
|
|
251
1137
|
toGeometry(): BufferGeometry;
|
|
252
1138
|
}
|
|
253
1139
|
|
|
254
|
-
export { Heightfield, type IHeightfieldOptions, type IHeightfieldOrigin, type IHeightfieldSamplerOptions, type IHeightfieldWorldPassOptions, type IWorldCapabilities, type IWorldTileColliderInput, type IWorldTilesTopologyObservation, TerrainTiles, getWorldCapabilities };
|
|
1140
|
+
export { Heightfield, type IAdmissionBudget, type IHeightfieldOptions, type IHeightfieldOrigin, type IHeightfieldSamplerOptions, type IHeightfieldWorldPassOptions, type ILoadTerrainSplatOptions, type IShadowRegion, type ITerrainSplatLayer, type ITerrainSplatMaskedLayer, type ITerrainSplatTable, type IWorldAsset, type IWorldAssetBounds, type IWorldAssetLod, type IWorldCapabilities, type IWorldCell, type IWorldCellsBudget, type IWorldCellsFollow, type IWorldCellsLoadOptions, type IWorldCellsStats, type IWorldCellsTerrainOptions, type IWorldExtent, type IWorldPackage, type IWorldPackageError, type IWorldPackageValidationOptions, type IWorldRun, type IWorldTerrain, type IWorldTileColliderInput, type IWorldTilesTopologyObservation, TERRAIN_VALIDATE_FLAG, TERRAIN_VALIDATE_MARKER, TerrainTiles, WorldCells, type WorldPackageErrorCode, cellPlacements, getWorldCapabilities, heightSamplerFromHeightmap, loadTerrainSplat, loadWorldHeightmap, terrainValidationRequested, validateWorldPackage };
|