@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/THIRD_PARTY_NOTICES.md +86 -0
  4. package/dist/budget-governor.d.ts +154 -0
  5. package/dist/camera-budget-governor.d.ts +234 -0
  6. package/dist/chunk-cache-budget.d.ts +156 -0
  7. package/dist/chunk-fetch-scheduler.d.ts +168 -0
  8. package/dist/chunk-loader.d.ts +85 -0
  9. package/dist/compute-sorter.d.ts +131 -0
  10. package/dist/create-splat-renderer.d.ts +99 -0
  11. package/dist/dataset-source.d.ts +55 -0
  12. package/dist/depth-of-field.d.ts +73 -0
  13. package/dist/effects.d.ts +337 -0
  14. package/dist/effects.js +180 -0
  15. package/dist/effects.js.map +1 -0
  16. package/dist/formats/ksplat/index.d.ts +8 -0
  17. package/dist/formats/ksplat/parse-ksplat.d.ts +9 -0
  18. package/dist/formats/ksplat.js +167 -0
  19. package/dist/formats/ksplat.js.map +1 -0
  20. package/dist/formats/lcc/collision-mesh.d.ts +37 -0
  21. package/dist/formats/lcc/collision-partition.d.ts +35 -0
  22. package/dist/formats/lcc/index.d.ts +16 -0
  23. package/dist/formats/lcc/lcc.d.ts +28 -0
  24. package/dist/formats/lcc/lcc2-transform.d.ts +18 -0
  25. package/dist/formats/lcc/lcc2.d.ts +4 -0
  26. package/dist/formats/lcc/parse-collision-lci.d.ts +28 -0
  27. package/dist/formats/lcc/parse-lcc.d.ts +177 -0
  28. package/dist/formats/lcc/parse-mesh-ply.d.ts +21 -0
  29. package/dist/formats/lcc.js +969 -0
  30. package/dist/formats/lcc.js.map +1 -0
  31. package/dist/formats/ply/index.d.ts +12 -0
  32. package/dist/formats/ply/parse-compressed-ply.d.ts +16 -0
  33. package/dist/formats/ply/parse-splat-ply.d.ts +46 -0
  34. package/dist/formats/ply.js +230 -0
  35. package/dist/formats/ply.js.map +1 -0
  36. package/dist/formats/rad/frontier-worker-protocol.d.ts +177 -0
  37. package/dist/formats/rad/index.d.ts +10 -0
  38. package/dist/formats/rad/parse-rad.d.ts +88 -0
  39. package/dist/formats/rad/rad-column-decoders.d.ts +15 -0
  40. package/dist/formats/rad/rad-foveated-source.d.ts +71 -0
  41. package/dist/formats/rad/rad-parent-sizes.d.ts +45 -0
  42. package/dist/formats/rad/rad.d.ts +132 -0
  43. package/dist/formats/rad.js +1098 -0
  44. package/dist/formats/rad.js.map +1 -0
  45. package/dist/formats/sog/index.d.ts +9 -0
  46. package/dist/formats/sog/parse-sog.d.ts +45 -0
  47. package/dist/formats/sog.js +257 -0
  48. package/dist/formats/sog.js.map +1 -0
  49. package/dist/formats/splat/index.d.ts +8 -0
  50. package/dist/formats/splat/parse-splat.d.ts +10 -0
  51. package/dist/formats/splat.js +39 -0
  52. package/dist/formats/splat.js.map +1 -0
  53. package/dist/formats/spz/index.d.ts +8 -0
  54. package/dist/formats/spz/parse-spz.d.ts +10 -0
  55. package/dist/formats/spz.js +218 -0
  56. package/dist/formats/spz.js.map +1 -0
  57. package/dist/frontier-worker-CAnK_Cmu.js +566 -0
  58. package/dist/frontier-worker-CAnK_Cmu.js.map +1 -0
  59. package/dist/half-float-Cs4FDq8Q.js +30 -0
  60. package/dist/half-float-Cs4FDq8Q.js.map +1 -0
  61. package/dist/half-float.d.ts +15 -0
  62. package/dist/index.d.ts +54 -0
  63. package/dist/index.js +9054 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/load-scene.d.ts +50 -0
  66. package/dist/load-worker-protocol.d.ts +82 -0
  67. package/dist/loading-wLo8vRbA.js +102 -0
  68. package/dist/loading-wLo8vRbA.js.map +1 -0
  69. package/dist/loading.d.ts +150 -0
  70. package/dist/lod-manifest.d.ts +61 -0
  71. package/dist/lod-scheduler.d.ts +229 -0
  72. package/dist/lod-source.d.ts +207 -0
  73. package/dist/logging-BfPdd7NJ.js +19 -0
  74. package/dist/logging-BfPdd7NJ.js.map +1 -0
  75. package/dist/logging.d.ts +37 -0
  76. package/dist/one-shot-worker-G80btnth.js +664 -0
  77. package/dist/one-shot-worker-G80btnth.js.map +1 -0
  78. package/dist/orientation.d.ts +35 -0
  79. package/dist/ply-header-BwgExCn-.js +125 -0
  80. package/dist/ply-header-BwgExCn-.js.map +1 -0
  81. package/dist/ply-header.d.ts +87 -0
  82. package/dist/radix-sort.d.ts +28 -0
  83. package/dist/radix-sorter.d.ts +46 -0
  84. package/dist/relighting.d.ts +50 -0
  85. package/dist/selection-volume.d.ts +87 -0
  86. package/dist/sh-pack-D5wAe5gg.js +54 -0
  87. package/dist/sh-pack-D5wAe5gg.js.map +1 -0
  88. package/dist/sh-pack.d.ts +49 -0
  89. package/dist/sort-scheduler.d.ts +62 -0
  90. package/dist/sort-worker-protocol.d.ts +37 -0
  91. package/dist/sorter.d.ts +31 -0
  92. package/dist/source-transform.d.ts +57 -0
  93. package/dist/splat-budget-PSojLJPO.js +157 -0
  94. package/dist/splat-budget-PSojLJPO.js.map +1 -0
  95. package/dist/splat-budget.d.ts +404 -0
  96. package/dist/splat-data-BZM5dhhx.js +10 -0
  97. package/dist/splat-data-BZM5dhhx.js.map +1 -0
  98. package/dist/splat-data.d.ts +150 -0
  99. package/dist/splat-depth-pack.d.ts +39 -0
  100. package/dist/splat-mesh-material.d.ts +255 -0
  101. package/dist/splat-mesh-picking.d.ts +87 -0
  102. package/dist/splat-mesh-pool.d.ts +241 -0
  103. package/dist/splat-mesh-types-8QDjTbZO.js +390 -0
  104. package/dist/splat-mesh-types-8QDjTbZO.js.map +1 -0
  105. package/dist/splat-mesh-types.d.ts +390 -0
  106. package/dist/splat-mesh.d.ts +737 -0
  107. package/dist/splat-modifier-stack.d.ts +38 -0
  108. package/dist/splat-modifier.d.ts +156 -0
  109. package/dist/splat-partition.d.ts +37 -0
  110. package/dist/splat-query.d.ts +51 -0
  111. package/dist/splat-scene.d.ts +127 -0
  112. package/dist/splat-sort-bounds.d.ts +11 -0
  113. package/dist/static-lod-splat-mesh.d.ts +53 -0
  114. package/dist/static-lod-worker-D2fyqQZO.js +474 -0
  115. package/dist/static-lod-worker-D2fyqQZO.js.map +1 -0
  116. package/dist/static-lod-worker-protocol.d.ts +37 -0
  117. package/dist/static-lod.d.ts +24 -0
  118. package/dist/storage-attribute-mirror.d.ts +103 -0
  119. package/dist/streamed-splat-mesh-utils.d.ts +146 -0
  120. package/dist/streamed-splat-mesh.d.ts +985 -0
  121. package/dist/unified-splat-renderer.d.ts +205 -0
  122. package/dist/webgpu-limits.d.ts +133 -0
  123. package/dist/work-buffer-gather.d.ts +119 -0
  124. package/dist/work-buffer-material.d.ts +42 -0
  125. package/dist/worker-sorter.d.ts +57 -0
  126. package/dist/xr-view.d.ts +92 -0
  127. 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
+ [![CI](https://github.com/Voluma-ai/vlam/actions/workflows/ci.yml/badge.svg)](https://github.com/Voluma-ai/vlam/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/@voluma/vlam.svg)](https://www.npmjs.com/package/@voluma/vlam)
7
+ [![demo](https://img.shields.io/badge/demo-live-4c1.svg)](https://vlam.voluma.ai)
8
+ [![license](https://img.shields.io/github/license/Voluma-ai/vlam.svg)](./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
+ }