@voluma/vlam 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/LICENSE +21 -0
- package/README.md +60 -0
- package/THIRD_PARTY_NOTICES.md +86 -0
- package/dist/budget-governor.d.ts +154 -0
- package/dist/camera-budget-governor.d.ts +234 -0
- package/dist/chunk-cache-budget.d.ts +156 -0
- package/dist/chunk-fetch-scheduler.d.ts +168 -0
- package/dist/chunk-loader.d.ts +85 -0
- package/dist/compute-sorter.d.ts +131 -0
- package/dist/create-splat-renderer.d.ts +99 -0
- package/dist/dataset-source.d.ts +55 -0
- package/dist/depth-of-field.d.ts +73 -0
- package/dist/effects.d.ts +337 -0
- package/dist/effects.js +180 -0
- package/dist/effects.js.map +1 -0
- package/dist/formats/ksplat/index.d.ts +8 -0
- package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
- package/dist/formats/ksplat.js +167 -0
- package/dist/formats/ksplat.js.map +1 -0
- package/dist/formats/lcc/collision-mesh.d.ts +37 -0
- package/dist/formats/lcc/collision-partition.d.ts +35 -0
- package/dist/formats/lcc/index.d.ts +16 -0
- package/dist/formats/lcc/lcc.d.ts +28 -0
- package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
- package/dist/formats/lcc/lcc2.d.ts +4 -0
- package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
- package/dist/formats/lcc/parse-lcc.d.ts +177 -0
- package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
- package/dist/formats/lcc.js +969 -0
- package/dist/formats/lcc.js.map +1 -0
- package/dist/formats/ply/index.d.ts +12 -0
- package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
- package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
- package/dist/formats/ply.js +230 -0
- package/dist/formats/ply.js.map +1 -0
- package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
- package/dist/formats/rad/index.d.ts +10 -0
- package/dist/formats/rad/parse-rad.d.ts +88 -0
- package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
- package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
- package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
- package/dist/formats/rad/rad.d.ts +132 -0
- package/dist/formats/rad.js +1098 -0
- package/dist/formats/rad.js.map +1 -0
- package/dist/formats/sog/index.d.ts +9 -0
- package/dist/formats/sog/parse-sog.d.ts +45 -0
- package/dist/formats/sog.js +257 -0
- package/dist/formats/sog.js.map +1 -0
- package/dist/formats/splat/index.d.ts +8 -0
- package/dist/formats/splat/parse-splat.d.ts +10 -0
- package/dist/formats/splat.js +39 -0
- package/dist/formats/splat.js.map +1 -0
- package/dist/formats/spz/index.d.ts +8 -0
- package/dist/formats/spz/parse-spz.d.ts +10 -0
- package/dist/formats/spz.js +218 -0
- package/dist/formats/spz.js.map +1 -0
- package/dist/frontier-worker-CAnK_Cmu.js +566 -0
- package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
- package/dist/half-float-Cs4FDq8Q.js +30 -0
- package/dist/half-float-Cs4FDq8Q.js.map +1 -0
- package/dist/half-float.d.ts +15 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +9054 -0
- package/dist/index.js.map +1 -0
- package/dist/load-scene.d.ts +50 -0
- package/dist/load-worker-protocol.d.ts +82 -0
- package/dist/loading-wLo8vRbA.js +102 -0
- package/dist/loading-wLo8vRbA.js.map +1 -0
- package/dist/loading.d.ts +150 -0
- package/dist/lod-manifest.d.ts +61 -0
- package/dist/lod-scheduler.d.ts +229 -0
- package/dist/lod-source.d.ts +207 -0
- package/dist/logging-BfPdd7NJ.js +19 -0
- package/dist/logging-BfPdd7NJ.js.map +1 -0
- package/dist/logging.d.ts +37 -0
- package/dist/one-shot-worker-G80btnth.js +664 -0
- package/dist/one-shot-worker-G80btnth.js.map +1 -0
- package/dist/orientation.d.ts +35 -0
- package/dist/ply-header-BwgExCn-.js +125 -0
- package/dist/ply-header-BwgExCn-.js.map +1 -0
- package/dist/ply-header.d.ts +87 -0
- package/dist/radix-sort.d.ts +28 -0
- package/dist/radix-sorter.d.ts +46 -0
- package/dist/relighting.d.ts +50 -0
- package/dist/selection-volume.d.ts +87 -0
- package/dist/sh-pack-D5wAe5gg.js +54 -0
- package/dist/sh-pack-D5wAe5gg.js.map +1 -0
- package/dist/sh-pack.d.ts +49 -0
- package/dist/sort-scheduler.d.ts +62 -0
- package/dist/sort-worker-protocol.d.ts +37 -0
- package/dist/sorter.d.ts +31 -0
- package/dist/source-transform.d.ts +57 -0
- package/dist/splat-budget-PSojLJPO.js +157 -0
- package/dist/splat-budget-PSojLJPO.js.map +1 -0
- package/dist/splat-budget.d.ts +404 -0
- package/dist/splat-data-BZM5dhhx.js +10 -0
- package/dist/splat-data-BZM5dhhx.js.map +1 -0
- package/dist/splat-data.d.ts +150 -0
- package/dist/splat-depth-pack.d.ts +39 -0
- package/dist/splat-mesh-material.d.ts +255 -0
- package/dist/splat-mesh-picking.d.ts +87 -0
- package/dist/splat-mesh-pool.d.ts +241 -0
- package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
- package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
- package/dist/splat-mesh-types.d.ts +390 -0
- package/dist/splat-mesh.d.ts +737 -0
- package/dist/splat-modifier-stack.d.ts +38 -0
- package/dist/splat-modifier.d.ts +156 -0
- package/dist/splat-partition.d.ts +37 -0
- package/dist/splat-query.d.ts +51 -0
- package/dist/splat-scene.d.ts +127 -0
- package/dist/splat-sort-bounds.d.ts +11 -0
- package/dist/static-lod-splat-mesh.d.ts +53 -0
- package/dist/static-lod-worker-D2fyqQZO.js +474 -0
- package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
- package/dist/static-lod-worker-protocol.d.ts +37 -0
- package/dist/static-lod.d.ts +24 -0
- package/dist/storage-attribute-mirror.d.ts +103 -0
- package/dist/streamed-splat-mesh-utils.d.ts +146 -0
- package/dist/streamed-splat-mesh.d.ts +985 -0
- package/dist/unified-splat-renderer.d.ts +205 -0
- package/dist/webgpu-limits.d.ts +133 -0
- package/dist/work-buffer-gather.d.ts +119 -0
- package/dist/work-buffer-material.d.ts +42 -0
- package/dist/worker-sorter.d.ts +57 -0
- package/dist/xr-view.d.ts +92 -0
- package/package.json +135 -21
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VOLUMA B.V.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# VLAM!
|
|
2
|
+
|
|
3
|
+
<!-- Image shields. Do not flatten CI badges to text links. -->
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Voluma-ai/vlam/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@voluma/vlam)
|
|
7
|
+
[](https://vlam.voluma.ai)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
|
|
10
|
+
A WebGPU Gaussian Splat viewer for three.js.
|
|
11
|
+
|
|
12
|
+
**Docs and demo:** [https://vlam.voluma.ai](https://vlam.voluma.ai) · [get started](site/get-started.md) · [guides](docs/guide/README.md)
|
|
13
|
+
|
|
14
|
+
Local site and generated API: `npm run dev` (http://localhost:5170, viewer at `/demo/`).
|
|
15
|
+
|
|
16
|
+
> Package `@voluma/vlam`, public **0.2.0**. APIs may still move before 1.0.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @voluma/vlam three
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`three` is a peer dependency (`>= 0.185.0`).
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import * as THREE from 'three/webgpu';
|
|
30
|
+
import { SplatMesh, createSplatRenderer, loadScene } from '@voluma/vlam';
|
|
31
|
+
|
|
32
|
+
const renderer = await createSplatRenderer();
|
|
33
|
+
renderer.setSize(innerWidth, innerHeight);
|
|
34
|
+
document.body.appendChild(renderer.domElement);
|
|
35
|
+
|
|
36
|
+
const scene = new THREE.Scene();
|
|
37
|
+
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 100);
|
|
38
|
+
camera.position.set(1, 0.5, 1.4);
|
|
39
|
+
camera.lookAt(0, 0, 0);
|
|
40
|
+
|
|
41
|
+
const splats = new SplatMesh(await loadScene('/scene.sog'));
|
|
42
|
+
scene.add(splats);
|
|
43
|
+
|
|
44
|
+
renderer.setAnimationLoop(() => {
|
|
45
|
+
splats.update(camera, renderer);
|
|
46
|
+
renderer.render(scene, camera);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Formats: `.sog`, `.ply`, `.spz`, `.splat`, `.ksplat`, `.rad`. Large scenes: `StreamedSplatMesh`.
|
|
51
|
+
|
|
52
|
+
## Contributing
|
|
53
|
+
|
|
54
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/architecture.md](docs/architecture.md). By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md). Security reports: see [SECURITY.md](SECURITY.md).
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
[MIT](LICENSE). Third-party notices: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
59
|
+
|
|
60
|
+
Made in the EU by [Voluma](https://voluma.ai).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The published package bundles no third-party code as-is; [three](https://github.com/mrdoob/three.js)
|
|
4
|
+
(MIT) is a peer dependency and is never included in the bundle. The notices
|
|
5
|
+
below cover algorithms adapted, with substantial modification, into original
|
|
6
|
+
source in this repository.
|
|
7
|
+
|
|
8
|
+
## PlayCanvas Engine
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2011-2026 PlayCanvas Ltd.
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
## Spark
|
|
31
|
+
|
|
32
|
+
https://github.com/sparkjsdev/spark
|
|
33
|
+
|
|
34
|
+
The `.rad`/`.radc` LOD tree traversal in `src/lib/formats/rad/` (notably the
|
|
35
|
+
priority-frontier cut and its `new_compute_pixel_scale` foveation weighting)
|
|
36
|
+
is a TypeScript port, with substantial modification, of Spark's Rust worker
|
|
37
|
+
(`rust/spark-worker-rs/src/lod_tree.rs`).
|
|
38
|
+
|
|
39
|
+
Copyright © 2025 WORLD LABS TECHNOLOGIES, INC.
|
|
40
|
+
|
|
41
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
42
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
43
|
+
in the Software without restriction, including without limitation the rights
|
|
44
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
45
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
46
|
+
furnished to do so, subject to the following conditions:
|
|
47
|
+
|
|
48
|
+
The above copyright notice and this permission notice shall be included in
|
|
49
|
+
all copies or substantial portions of the Software.
|
|
50
|
+
|
|
51
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
52
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
53
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
54
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
55
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
56
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
57
|
+
THE SOFTWARE.
|
|
58
|
+
|
|
59
|
+
## WebGPU-Radix-Sort
|
|
60
|
+
|
|
61
|
+
https://github.com/kishimisu/WebGPU-Radix-Sort
|
|
62
|
+
|
|
63
|
+
The stable ranked-scatter algorithm is based on WebGPU-Radix-Sort by
|
|
64
|
+
kishimisu, declared MIT-licensed in its `package.json` (the repository ships
|
|
65
|
+
no LICENSE file of its own; the notice below reproduces the standard MIT
|
|
66
|
+
terms on the author's behalf).
|
|
67
|
+
|
|
68
|
+
Copyright (c) kishimisu
|
|
69
|
+
|
|
70
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
71
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
72
|
+
in the Software without restriction, including without limitation the rights
|
|
73
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
74
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
75
|
+
furnished to do so, subject to the following conditions:
|
|
76
|
+
|
|
77
|
+
The above copyright notice and this permission notice shall be included in
|
|
78
|
+
all copies or substantial portions of the Software.
|
|
79
|
+
|
|
80
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
81
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
82
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
83
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
84
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
85
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
86
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared splat-budget governance across multiple streamed meshes.
|
|
3
|
+
*
|
|
4
|
+
* A single `StreamedSplatMesh` keeps itself within a per-device budget, but a
|
|
5
|
+
* host that shows several streamed scenes at once (a main capture plus marker
|
|
6
|
+
* or inset meshes) must not let each mesh claim the whole device budget -
|
|
7
|
+
* their pools are separate, so the costs add. Historically hosts hand-tuned
|
|
8
|
+
* this ("shrink the main mesh to 0.7 when markers exist"); the
|
|
9
|
+
* {@link BudgetGovernor} makes it a first-class policy: register each mesh
|
|
10
|
+
* with a priority weight and the governor splits one total budget across the
|
|
11
|
+
* members, reallocating when membership, weights, or the total change.
|
|
12
|
+
*
|
|
13
|
+
* The governor steers members exclusively through their public
|
|
14
|
+
* `setBudget`, so every downstream consumer of a member's budget - the
|
|
15
|
+
* flat-leaf `LodScheduler`, the LCC2 octree cut, and the RAD page-table draw
|
|
16
|
+
* target - sees the governed value through the exact path an explicit host
|
|
17
|
+
* `setBudget` call would take. Meshes never registered with a governor are
|
|
18
|
+
* completely unaffected.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Anything the governor can steer. `StreamedSplatMesh` satisfies this
|
|
22
|
+
* structurally; a custom member only needs the same clamp-and-report
|
|
23
|
+
* `setBudget` contract.
|
|
24
|
+
*/
|
|
25
|
+
export interface BudgetGovernedMember {
|
|
26
|
+
/** The member's current effective active-splat budget. */
|
|
27
|
+
readonly budget: number;
|
|
28
|
+
/**
|
|
29
|
+
* Applies a budget and returns the value actually in effect - which may be
|
|
30
|
+
* lower than asked when the member clamps to a fixed ceiling (for
|
|
31
|
+
* `StreamedSplatMesh`, its `maxBudget`).
|
|
32
|
+
*/
|
|
33
|
+
setBudget(budget: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* The ceiling `setBudget` clamps to, when the member knows one
|
|
36
|
+
* (`StreamedSplatMesh.maxBudget`). **Advisory only** - allocation still
|
|
37
|
+
* discovers real caps from `setBudget`'s return value, so a member that
|
|
38
|
+
* omits this is governed exactly as well.
|
|
39
|
+
*/
|
|
40
|
+
readonly maxBudget?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Options for {@link BudgetGovernor}. */
|
|
43
|
+
export interface BudgetGovernorOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Total active-splat budget shared by all members. Defaults to the
|
|
46
|
+
* per-device {@link resolveSplatBudget} - i.e. the group as a whole gets
|
|
47
|
+
* what one mesh alone would get today.
|
|
48
|
+
*/
|
|
49
|
+
totalBudget?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Grow dead-band as a fraction of a member's current budget (default
|
|
52
|
+
* `0.1`). A reallocation that would *raise* a member's budget by no more
|
|
53
|
+
* than this fraction is skipped, so brief membership churn (a marker mesh
|
|
54
|
+
* appearing for a moment) does not thrash LOD schedules. Shrinks always
|
|
55
|
+
* apply immediately - that is what keeps `sum(member budgets) ≤ total` an
|
|
56
|
+
* invariant rather than a goal.
|
|
57
|
+
*/
|
|
58
|
+
hysteresis?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Splits one total splat budget across registered members by priority weight.
|
|
62
|
+
*
|
|
63
|
+
* Allocation is weighted and cap-aware: a member whose `setBudget` clamps
|
|
64
|
+
* below its weighted share (a small scene, or a mesh with a small pool)
|
|
65
|
+
* releases the difference to the remaining members, so the total is spent
|
|
66
|
+
* where it can buy detail. Reallocation runs automatically on
|
|
67
|
+
* register/unregister and on weight or total changes.
|
|
68
|
+
*
|
|
69
|
+
* A member at `weight: 0` is **suspended**: held at
|
|
70
|
+
* {@link SUSPENDED_BUDGET}, excluded from the weighted split, and its whole
|
|
71
|
+
* share released to the others - Spark's `lodScale: 0` hidden tier, without
|
|
72
|
+
* unregistering (so the mesh stays warm and re-weighting it costs nothing).
|
|
73
|
+
* Note that a suspended member is not *free*: its pool was allocated at
|
|
74
|
+
* construction and is never released, and a streamed mesh keeps its pinned
|
|
75
|
+
* coarse shell resident, so it consumes ≈0 of the budget rather than exactly 0.
|
|
76
|
+
* To give the memory back, dispose the mesh.
|
|
77
|
+
*
|
|
78
|
+
* For camera-driven weights - nearby meshes automatically taking a larger
|
|
79
|
+
* share - see `CameraBudgetGovernor`, which drives this class.
|
|
80
|
+
*
|
|
81
|
+
* Invariant: the sum of budgets the governor has applied to active members
|
|
82
|
+
* never exceeds {@link totalBudget}.
|
|
83
|
+
*/
|
|
84
|
+
export declare class BudgetGovernor {
|
|
85
|
+
private readonly entries;
|
|
86
|
+
private total;
|
|
87
|
+
private readonly hysteresis;
|
|
88
|
+
constructor(options?: BudgetGovernorOptions);
|
|
89
|
+
/** The shared budget currently being split across members. */
|
|
90
|
+
get totalBudget(): number;
|
|
91
|
+
/** Replaces the shared total and reallocates. */
|
|
92
|
+
setTotalBudget(totalBudget: number): void;
|
|
93
|
+
/** Number of registered members. */
|
|
94
|
+
get size(): number;
|
|
95
|
+
/**
|
|
96
|
+
* Adds a member and reallocates the shared budget. The member's current
|
|
97
|
+
* budget is remembered and restored when it leaves the governor.
|
|
98
|
+
*
|
|
99
|
+
* @param member - The mesh (or compatible object) to govern.
|
|
100
|
+
* @param options - `weight` (default `1`): the member's share is
|
|
101
|
+
* proportional to its weight - e.g. main mesh `7`, marker `3` reproduces
|
|
102
|
+
* the old 0.7 host split. `0` registers the member suspended.
|
|
103
|
+
*/
|
|
104
|
+
register(member: BudgetGovernedMember, options?: {
|
|
105
|
+
weight?: number;
|
|
106
|
+
}): void;
|
|
107
|
+
/**
|
|
108
|
+
* Removes a member, restores the budget it had when it registered, and
|
|
109
|
+
* reallocates the total across the remaining members. No-op for a member
|
|
110
|
+
* that is not registered (so disposing hosts need not track membership).
|
|
111
|
+
*/
|
|
112
|
+
unregister(member: BudgetGovernedMember): void;
|
|
113
|
+
/**
|
|
114
|
+
* Changes a member's priority weight and reallocates. `0` suspends the
|
|
115
|
+
* member (see the class doc); any positive weight resumes it.
|
|
116
|
+
*/
|
|
117
|
+
setWeight(member: BudgetGovernedMember, weight: number): void;
|
|
118
|
+
/**
|
|
119
|
+
* Writes several weights, then reallocates **once**.
|
|
120
|
+
*
|
|
121
|
+
* Prefer this to a loop of {@link setWeight} whenever more than one weight
|
|
122
|
+
* changes together - as a camera-driven reweight does. Each reallocation
|
|
123
|
+
* pushes `setBudget` to every member, and for a streamed mesh that forces an
|
|
124
|
+
* LOD reschedule, so N separate calls cost N passes over the whole group to
|
|
125
|
+
* reach a state one pass would have produced.
|
|
126
|
+
*
|
|
127
|
+
* @param weights - `[member, weight]` pairs. Every member must be registered;
|
|
128
|
+
* unlisted members keep their current weight.
|
|
129
|
+
* @throws {Error} if any member is not registered - checked before anything
|
|
130
|
+
* is written, so a bad pair leaves every weight untouched.
|
|
131
|
+
* @throws {RangeError} if any weight is not a non-negative finite number.
|
|
132
|
+
*/
|
|
133
|
+
setWeights(weights: Iterable<readonly [BudgetGovernedMember, number]>): void;
|
|
134
|
+
/** The budget the governor last applied to a member, if registered. */
|
|
135
|
+
budgetOf(member: BudgetGovernedMember): number | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Recomputes and applies every member's share. Called automatically by all
|
|
138
|
+
* mutators; call it manually only if a member's internal ceiling changed
|
|
139
|
+
* outside the governor's view.
|
|
140
|
+
*/
|
|
141
|
+
reallocate(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Restores every member's pre-registration budget and empties the governor.
|
|
144
|
+
* The governor itself stays usable (dispose is just "unregister everyone").
|
|
145
|
+
*/
|
|
146
|
+
dispose(): void;
|
|
147
|
+
/** The entry for a registered member, or a thrown error naming the problem. */
|
|
148
|
+
private entryOf;
|
|
149
|
+
/**
|
|
150
|
+
* Pushes `target` to a member, with the grow dead-band. Returns true when
|
|
151
|
+
* the member clamped below its target (it is capped and cannot absorb more).
|
|
152
|
+
*/
|
|
153
|
+
private applyTarget;
|
|
154
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera-driven splat-budget weighting across several streamed meshes.
|
|
3
|
+
*
|
|
4
|
+
* {@link BudgetGovernor} splits one total by *priority*, which a host must
|
|
5
|
+
* choose and maintain. That is the wrong axis for a scene of markers: what
|
|
6
|
+
* makes a marker worth splats is that the camera is near it, and that changes
|
|
7
|
+
* every frame. Splitting a pool evenly instead (`pool / N`) gives the marker
|
|
8
|
+
* you fly up to a quarter of the budget it needs while three markers nobody is
|
|
9
|
+
* looking at hold the rest.
|
|
10
|
+
*
|
|
11
|
+
* {@link CameraBudgetGovernor} closes that loop: each update it measures every
|
|
12
|
+
* member's projected size from the camera, multiplies in the host's priority
|
|
13
|
+
* tier, and writes the resulting weights to a `BudgetGovernor` in one batch.
|
|
14
|
+
* Approaching one of four markers pulls budget off the far ones automatically.
|
|
15
|
+
*
|
|
16
|
+
* This is Spark's model, reached differently. Spark shares one `lodSplatCount`
|
|
17
|
+
* and biases meshes with a per-mesh `lodScale` (focused 2, adjacent 0.25,
|
|
18
|
+
* hidden 0); its GPU traversal then favors near, on-screen detail on its own.
|
|
19
|
+
* VLAM's meshes each own a pool, so the near/far bias has to be applied to the
|
|
20
|
+
* *budget* - which is what this class does, with `priority` playing the part of
|
|
21
|
+
* `lodScale`. (For a `.rad` page-table mesh, `StreamedSplatMesh.lodScale` is
|
|
22
|
+
* also available and is Spark's knob exactly.)
|
|
23
|
+
*
|
|
24
|
+
* Composition, not inheritance: `BudgetGovernor` stays a pure allocation
|
|
25
|
+
* policy with no camera and no frame lifecycle, and a host that wants fixed
|
|
26
|
+
* weights keeps using it directly.
|
|
27
|
+
*/
|
|
28
|
+
import * as THREE from 'three/webgpu';
|
|
29
|
+
import { BudgetGovernor, type BudgetGovernedMember, type BudgetGovernorOptions } from './budget-governor';
|
|
30
|
+
/**
|
|
31
|
+
* A governed member this class can measure. Every `SplatMesh` (and so every
|
|
32
|
+
* `StreamedSplatMesh`) satisfies it structurally - there is no extra host
|
|
33
|
+
* plumbing to write.
|
|
34
|
+
*/
|
|
35
|
+
export interface CameraBudgetMember extends BudgetGovernedMember {
|
|
36
|
+
/**
|
|
37
|
+
* The member's splat bounds in its own local frame. `StreamedSplatMesh`
|
|
38
|
+
* overrides this to the whole scene's bounds, which are known from the
|
|
39
|
+
* manifest - so weighting is correct from the first frame, before a single
|
|
40
|
+
* chunk has loaded.
|
|
41
|
+
*/
|
|
42
|
+
computeSplatBounds(): THREE.Box3;
|
|
43
|
+
/** Local→world transform, read after {@link updateWorldMatrix}. */
|
|
44
|
+
readonly matrixWorld: THREE.Matrix4;
|
|
45
|
+
/** `Object3D.updateWorldMatrix`; called so a fresh member is placed correctly. */
|
|
46
|
+
updateWorldMatrix(updateParents: boolean, updateChildren: boolean): void;
|
|
47
|
+
/**
|
|
48
|
+
* `Object3D.visible`. A hidden member is suspended (weight 0) - it draws
|
|
49
|
+
* nothing, so it should hold no budget. This is the member's own flag, not an
|
|
50
|
+
* ancestor walk: a host hiding a whole group should set `priority: 0`.
|
|
51
|
+
*/
|
|
52
|
+
readonly visible: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The visibility that actually decides whether the member's splats reach the
|
|
55
|
+
* screen, when that differs from `visible`. `SplatMesh` provides it: a
|
|
56
|
+
* `UnifiedSplatRenderer` forces `visible = false` on every source it owns
|
|
57
|
+
* (only to keep the regular scene draw from double-drawing them) while the
|
|
58
|
+
* source may be fully on screen through the unified draw - without this, the
|
|
59
|
+
* governor would suspend every unified source and freeze its streaming.
|
|
60
|
+
* When present it wins over `visible`.
|
|
61
|
+
*/
|
|
62
|
+
readonly effectiveVisibility?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Options for {@link CameraBudgetGovernor}. */
|
|
65
|
+
export interface CameraBudgetGovernorOptions extends BudgetGovernorOptions {
|
|
66
|
+
/**
|
|
67
|
+
* An existing governor to drive, when the host already has one (or wants to
|
|
68
|
+
* mix camera-weighted and fixed-weight members). By default one is built
|
|
69
|
+
* from the inherited {@link BudgetGovernorOptions}.
|
|
70
|
+
*/
|
|
71
|
+
governor?: BudgetGovernor;
|
|
72
|
+
/**
|
|
73
|
+
* Minimum milliseconds between reweights. Default `250`, matching
|
|
74
|
+
* `StreamedSplatMesh`'s own idle reschedule interval: a mesh cannot act on a
|
|
75
|
+
* budget change faster than it reschedules, so reweighting more often buys
|
|
76
|
+
* nothing and costs a forced reschedule on every member. Membership and
|
|
77
|
+
* priority changes bypass it.
|
|
78
|
+
*/
|
|
79
|
+
minIntervalMs?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Relative weight change below which a reweight is skipped entirely, as a
|
|
82
|
+
* fraction of the weight in effect. Default `0.15`.
|
|
83
|
+
*
|
|
84
|
+
* This sits *above* `BudgetGovernor`'s grow dead-band and does a different
|
|
85
|
+
* job: that one damps budget churn on a member, this one suppresses the
|
|
86
|
+
* reallocation altogether so an idling camera does no work at all.
|
|
87
|
+
*/
|
|
88
|
+
weightDeadband?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Exponent on projected size. `1` (default) weights by angular size -
|
|
91
|
+
* halving the distance doubles the share. `2` weights by projected *area*,
|
|
92
|
+
* which concentrates the budget harder on the nearest member.
|
|
93
|
+
*/
|
|
94
|
+
falloff?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Weight multiplier for a member outside the view frustum. Default `0.25`:
|
|
97
|
+
* suppressed, never starved. Off-screen members must keep enough budget for
|
|
98
|
+
* their coarse shell, or turning the camera exposes an unpainted region -
|
|
99
|
+
* the same foveate-don't-cull policy the `.rad` frontier traversal follows.
|
|
100
|
+
*/
|
|
101
|
+
offScreenWeight?: number;
|
|
102
|
+
/** Floor on the projected-size term, so a very distant member still holds a
|
|
103
|
+
* coarse shell. Default `0.05`. */
|
|
104
|
+
minWeight?: number;
|
|
105
|
+
/** Ceiling on the projected-size term, so a member the camera is inside
|
|
106
|
+
* cannot take the entire total. Default `8`. */
|
|
107
|
+
maxWeight?: number;
|
|
108
|
+
}
|
|
109
|
+
/** Per-member options for {@link CameraBudgetGovernor.register}. */
|
|
110
|
+
export interface CameraBudgetMemberOptions {
|
|
111
|
+
/**
|
|
112
|
+
* Host priority multiplied into the camera term - Spark's `lodScale` tiers:
|
|
113
|
+
* focused `2`, default `1`, adjacent `0.25`, hidden `0`. Default `1`.
|
|
114
|
+
* `0` suspends the member (see {@link BudgetGovernor}).
|
|
115
|
+
*/
|
|
116
|
+
priority?: number;
|
|
117
|
+
/**
|
|
118
|
+
* Pins this member's weight, opting it out of camera weighting while it still
|
|
119
|
+
* competes for the same total. Use it for a main scene that should hold a
|
|
120
|
+
* steady share while markers fight over the rest - e.g. `fixedWeight: 4`
|
|
121
|
+
* against markers averaging `1`.
|
|
122
|
+
*/
|
|
123
|
+
fixedWeight?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Weights a {@link BudgetGovernor}'s members by how large each one projects
|
|
127
|
+
* from the camera, so nearby meshes take budget from distant ones.
|
|
128
|
+
*
|
|
129
|
+
* ```js
|
|
130
|
+
* const governor = new CameraBudgetGovernor({ totalBudget: 4_000_000 });
|
|
131
|
+
* governor.register(main, { fixedWeight: 4 }); // steady share
|
|
132
|
+
* governor.register(markerA); // camera-weighted, priority 1
|
|
133
|
+
* governor.register(markerB);
|
|
134
|
+
*
|
|
135
|
+
* // once per frame, after the scene graph is up to date:
|
|
136
|
+
* governor.update(camera);
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* Every guarantee of the underlying governor still holds - most importantly
|
|
140
|
+
* that the applied budgets never sum above the total, and that unregistering a
|
|
141
|
+
* member restores the budget it had when it joined.
|
|
142
|
+
*
|
|
143
|
+
* **A member can only grow into a budget its pool can hold.** A streamed mesh
|
|
144
|
+
* allocates its pool once, from its construction budget, and clamps `setBudget`
|
|
145
|
+
* to it - so a marker built at a quarter of the total can never be given more
|
|
146
|
+
* than a quarter, however close the camera gets. Construct governed meshes with
|
|
147
|
+
* `maxBudget` set to the largest share they should ever reach, and price those
|
|
148
|
+
* ceilings with `estimateSplatPoolBytes` first: the pools cost their ceilings
|
|
149
|
+
* whatever the budget is split to.
|
|
150
|
+
*/
|
|
151
|
+
export declare class CameraBudgetGovernor {
|
|
152
|
+
private readonly entries;
|
|
153
|
+
private readonly budgetGovernor;
|
|
154
|
+
private readonly minIntervalMs;
|
|
155
|
+
private readonly weightDeadband;
|
|
156
|
+
private readonly falloff;
|
|
157
|
+
private readonly offScreenWeight;
|
|
158
|
+
private readonly minWeight;
|
|
159
|
+
private readonly maxWeight;
|
|
160
|
+
private lastUpdateAt;
|
|
161
|
+
/** Set by membership/priority changes: the next update ignores both damps. */
|
|
162
|
+
private forceNext;
|
|
163
|
+
constructor(options?: CameraBudgetGovernorOptions);
|
|
164
|
+
/** The governor this drives; use it for fixed-weight members and diagnostics. */
|
|
165
|
+
get governor(): BudgetGovernor;
|
|
166
|
+
/** The shared total being split, from the underlying governor. */
|
|
167
|
+
get totalBudget(): number;
|
|
168
|
+
/** Replaces the shared total (e.g. the presenting budget on `sessionstart`). */
|
|
169
|
+
setTotalBudget(totalBudget: number): void;
|
|
170
|
+
/** Number of camera-weighted members. */
|
|
171
|
+
get size(): number;
|
|
172
|
+
/**
|
|
173
|
+
* Adds a member, registers it with the underlying governor, and forces a
|
|
174
|
+
* reweight on the next {@link update}.
|
|
175
|
+
*
|
|
176
|
+
* @throws {Error} if the member is already registered here.
|
|
177
|
+
* @throws {RangeError} if `priority` or `fixedWeight` is invalid.
|
|
178
|
+
*/
|
|
179
|
+
register(member: CameraBudgetMember, options?: CameraBudgetMemberOptions): void;
|
|
180
|
+
/**
|
|
181
|
+
* Removes a member from this helper and the underlying governor, restoring
|
|
182
|
+
* the budget it had when it joined. No-op for an unknown member.
|
|
183
|
+
*/
|
|
184
|
+
unregister(member: CameraBudgetMember): void;
|
|
185
|
+
/**
|
|
186
|
+
* Changes a member's priority tier. Takes effect on the next {@link update},
|
|
187
|
+
* which is forced - a deliberate focus change should not wait out the
|
|
188
|
+
* interval or be swallowed by the dead-band.
|
|
189
|
+
*
|
|
190
|
+
* @throws {Error} if the member is not registered here.
|
|
191
|
+
* @throws {RangeError} if `priority` is negative or not finite.
|
|
192
|
+
*/
|
|
193
|
+
setPriority(member: CameraBudgetMember, priority: number): void;
|
|
194
|
+
/**
|
|
195
|
+
* Recomputes every member's weight from the camera and applies them in one
|
|
196
|
+
* batch. Call once per frame, after the scene graph is up to date.
|
|
197
|
+
*
|
|
198
|
+
* Needs no renderer: weights are a ratio, so viewport size cancels out.
|
|
199
|
+
*
|
|
200
|
+
* Skipped - returning `false` - when called inside `minIntervalMs` of the
|
|
201
|
+
* last reweight, or when no member's weight moved by more than
|
|
202
|
+
* `weightDeadband`. Membership and priority changes force it through both.
|
|
203
|
+
*
|
|
204
|
+
* @param camera - The view detail should follow. In an immersive session pass
|
|
205
|
+
* the head/`ArrayCamera`, not the idle application camera.
|
|
206
|
+
* @param now - Timestamp in ms on the `performance.now` clock; defaults to it.
|
|
207
|
+
* @returns whether weights were reapplied.
|
|
208
|
+
*/
|
|
209
|
+
update(camera: THREE.Camera, now?: number): boolean;
|
|
210
|
+
/** The weight currently written for a member, if registered here. */
|
|
211
|
+
weightOf(member: CameraBudgetMember): number | undefined;
|
|
212
|
+
/** The budget the underlying governor last applied to a member. */
|
|
213
|
+
budgetOf(member: CameraBudgetMember): number | undefined;
|
|
214
|
+
/**
|
|
215
|
+
* Unregisters every member, restoring the budget each had when it joined.
|
|
216
|
+
* The helper stays usable afterwards. A governor passed in by the host keeps
|
|
217
|
+
* any members the host registered on it directly.
|
|
218
|
+
*/
|
|
219
|
+
dispose(): void;
|
|
220
|
+
/**
|
|
221
|
+
* A member's weight: `priority × clamp((radius / distance) ^ falloff) ×
|
|
222
|
+
* offScreen`.
|
|
223
|
+
*
|
|
224
|
+
* `radius / distance` is the tangent of the member's half angular size - the
|
|
225
|
+
* same `size / distance` measure Spark's `pixel_scale` traversal ranks nodes
|
|
226
|
+
* by, one level up at whole-mesh granularity. Distance is measured to the
|
|
227
|
+
* bounding sphere's *surface*, so a large mesh is not penalized for having a
|
|
228
|
+
* distant center, and is floored so a camera inside the bounds saturates at
|
|
229
|
+
* `maxWeight` rather than dividing by zero.
|
|
230
|
+
*/
|
|
231
|
+
private weightFor;
|
|
232
|
+
/** Whether a weight moved enough to be worth a reallocation. */
|
|
233
|
+
private isSignificant;
|
|
234
|
+
}
|