@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
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Device profiling and the default active-splat budget.
3
+ *
4
+ * Streaming is splat-count budgeted rather than byte budgeted: every cost in
5
+ * the renderer (pool textures, sort buffers) scales with the number of
6
+ * resident splats - roughly 64 bytes of GPU memory per splat (three RGBA32F
7
+ * data textures + one RGBA8, plus the sort index/order/bucket buffers), or
8
+ * about 48 bytes when `poolFloatTextures: 'float16'` halves centers and
9
+ * covarianceA; plus about 52 more bytes of CPU-side backing per splat of
10
+ * pool capacity. Capping the active splat count therefore caps memory
11
+ * directly, which is what matters on mobile GPUs.
12
+ */
13
+ import type { StreamedSplatFormat } from './loading';
14
+ /**
15
+ * Desktop GPU capability class for budget / quality defaults.
16
+ *
17
+ * Mobile still keys off {@link SplatDeviceProfile.isMobile}; this splits
18
+ * non-mobile machines that would otherwise share the ~8M workstation path.
19
+ * Unset means "unknown" - keep the pre-existing desktop defaults (fail open).
20
+ */
21
+ export type SplatGpuClass = 'discrete' | 'integrated' | 'fallback';
22
+ /** Browser/device signals used for deterministic budget selection. */
23
+ export interface SplatDeviceProfile {
24
+ /** Coarse memory estimate in GiB, when exposed by the browser. */
25
+ deviceMemoryGb?: number;
26
+ /** Whether the runtime is an iOS/iPadOS-class device. */
27
+ isIOS?: boolean;
28
+ /** Whether the runtime is a phone/tablet-class device (includes iOS). */
29
+ isMobile?: boolean;
30
+ /**
31
+ * Whether the runtime is a standalone XR headset browser (Quest, Pico, …).
32
+ * Headsets are mobile-class GPUs asked to fill a stereo framebuffer larger
33
+ * than a 4K desktop, so they get their own, tighter defaults.
34
+ */
35
+ isHeadset?: boolean;
36
+ /**
37
+ * Whether this is a *budget* phone rather than a flagship one.
38
+ *
39
+ * `isMobile` cannot tell a Galaxy A51 from an iPhone 15 Pro, and the memory
40
+ * signal cannot either - Chrome privacy-caps `deviceMemory` at 8 GiB, so the
41
+ * flagship and the mid-ranger both land on the same ceiling despite an order
42
+ * of magnitude between their GPUs. A coarse signal, and a *downward* one: a
43
+ * reading of ≤4 is trustworthy because the cap only ever lowers it, while a
44
+ * reading of 8 says nothing. {@link hasWebGpu} is the sharper signal where it
45
+ * applies, since it names a difference in how the sort runs rather than
46
+ * guessing capability from RAM.
47
+ */
48
+ isLowPower?: boolean;
49
+ /**
50
+ * Whether the runtime exposes WebGPU at all.
51
+ *
52
+ * `false` means the renderer will take the WebGL2 fallback, where the depth
53
+ * sort runs on the CPU rather than as a GPU compute pass - a different cost
54
+ * curve, not just a slower one. See {@link resolveSplatBudget}.
55
+ *
56
+ * Detected from the presence of `navigator.gpu`, which is a *browser support*
57
+ * signal rather than an adapter one: a runtime that exposes the API but whose
58
+ * `requestAdapter` fails (driver blocklist) still reports `true` here and will
59
+ * fall back to WebGL2 anyway. A host that knows its renderer's real backend
60
+ * can correct this by passing its own profile.
61
+ */
62
+ hasWebGpu?: boolean;
63
+ /**
64
+ * Desktop GPU class from {@link probeSplatGpuClass} / {@link classifySplatGpuClass}.
65
+ * Omit when unknown so workstations without adapter info keep the 8M path.
66
+ */
67
+ gpuClass?: SplatGpuClass;
68
+ }
69
+ /** Adapter identifying fields used by {@link classifySplatGpuClass}. */
70
+ export interface SplatGpuAdapterInfo {
71
+ vendor?: string;
72
+ architecture?: string;
73
+ device?: string;
74
+ description?: string;
75
+ isFallbackAdapter?: boolean;
76
+ }
77
+ /**
78
+ * How expensive a format's splats are *per splat*, which is not the same across
79
+ * formats and is the reason one mobile budget cannot serve them all.
80
+ *
81
+ * - `'sampled'` - coarser LOD levels are subsamples of the same surface, so a
82
+ * splat costs about the same however deep the cut is. Streamed SOG.
83
+ * - `'lcc'` - XGRIDS' levels are decimated *alternatives* whose splats merge
84
+ * into wide flat discs (see {@link liftBudgetToFinestLevel}). Cost per splat
85
+ * therefore *rises* as the budget falls, because a tighter budget serves
86
+ * coarser levels and each of their splats covers more pixels. `.rad`, `.lcc`,
87
+ * `.lcc2`.
88
+ *
89
+ * Measured on one iPhone 15 Pro, which is what the numbers below are worth:
90
+ * `oldtimers-route` (`.rad`) ran 31-39 fps at 750k and 45-50 at 600k, while
91
+ * `sandwijck` (streamed SOG) held 52-57 fps at 675k resident. Two streamed
92
+ * scenes, near-identical splat counts, ~20 fps apart - splat count is a poor
93
+ * proxy for cost, and the format is the only better signal available for free
94
+ * before a single byte is fetched.
95
+ */
96
+ export type SplatCostClass = 'lcc' | 'sampled';
97
+ /**
98
+ * Classifies a WebGPU adapter from its identifying strings.
99
+ *
100
+ * Heuristics only - browsers may blank fields for privacy. Prefer
101
+ * {@link probeSplatGpuClass} when both power-preference adapters are available.
102
+ */
103
+ export declare function classifySplatGpuClass(info: SplatGpuAdapterInfo | undefined): SplatGpuClass;
104
+ /** Minimal `navigator.gpu` surface for {@link probeSplatGpuClass}. */
105
+ export interface SplatGpuProbeEntry {
106
+ requestAdapter(options?: {
107
+ powerPreference?: 'low-power' | 'high-performance';
108
+ }): Promise<SplatGpuProbeAdapter | null>;
109
+ }
110
+ /** Adapter handle with optional `.info` (WebGPU GPUAdapterInfo). */
111
+ export interface SplatGpuProbeAdapter {
112
+ readonly info?: SplatGpuAdapterInfo;
113
+ }
114
+ /**
115
+ * Probes WebGPU adapters to choose a {@link SplatGpuClass}.
116
+ *
117
+ * Requests both `low-power` and `high-performance` adapters when possible. A
118
+ * hybrid laptop that exposes two different devices is `discrete`; a single
119
+ * shared / unified GPU (Apple Silicon, many iGPU-only machines) is classified
120
+ * from {@link classifySplatGpuClass}. Returns `undefined` when WebGPU is absent
121
+ * or the probe fails, so budget resolution keeps the legacy desktop path.
122
+ */
123
+ export declare function probeSplatGpuClass(gpu?: SplatGpuProbeEntry | null | undefined): Promise<SplatGpuClass | undefined>;
124
+ /**
125
+ * Whether fill-rate / laptop-class defaults apply (mobile, or desktop
126
+ * integrated / software fallback). Exported so hosts and the demo share one
127
+ * predicate for performance-mode defaults and adaptive DPR.
128
+ */
129
+ export declare function isFillConstrainedSplatDevice(profile?: SplatDeviceProfile | undefined): boolean;
130
+ /** Reads the available browser device signals without requiring a DOM runtime. */
131
+ export declare function detectSplatDeviceProfile(): SplatDeviceProfile | undefined;
132
+ /** Scene-side signals for {@link resolveSplatBudget}. */
133
+ export interface SplatBudgetOptions {
134
+ /**
135
+ * The format about to be loaded, which selects the {@link SplatCostClass}
136
+ * whose mobile tier applies. Omit when it is not yet known - the `'sampled'`
137
+ * numbers are what every caller resolved before cost classes existed.
138
+ */
139
+ format?: StreamedSplatFormat;
140
+ /**
141
+ * A ceiling on the resolved default, for callers that want to tighten without
142
+ * overriding what this function knows.
143
+ *
144
+ * `override` is absolute: it wins over the device tier, the cost class and
145
+ * everything else, because a caller who names a number has said they know
146
+ * better. That is the right contract and the wrong tool for "the same as
147
+ * usual, but no more than N" - which is what a performance toggle or a host
148
+ * default actually means. Pinning a number there has twice shipped as a bug:
149
+ * a demo performance mode that *raised* the load on the weakest device tested,
150
+ * and a host default that bypassed every device tier.
151
+ *
152
+ * Applied only when `override` is omitted, and only downward.
153
+ */
154
+ cap?: number;
155
+ }
156
+ /**
157
+ * Chooses a default active-splat budget for the current device.
158
+ *
159
+ * The default is derived from the coarsest device-memory signal the platform
160
+ * exposes; callers who know better (or want a hard cap for a mobile test on
161
+ * desktop) should pass an explicit override.
162
+ *
163
+ * @param override - Explicit budget in splats; wins over device detection when
164
+ * positive. Throws `RangeError` if it is not a positive finite number.
165
+ * @param profile - Device signals to decide from; defaults to detecting them.
166
+ * @param options - Scene signals. `format` selects the {@link SplatCostClass}
167
+ * whose mobile tier applies; omitting it keeps the `'sampled'` numbers, which
168
+ * are what every caller resolved before cost classes existed. `cap` is a
169
+ * ceiling on the *resolved default*, for callers who want to tighten without
170
+ * overriding - see {@link SplatBudgetOptions.cap}.
171
+ * @returns A splat-count budget, clamped to a sensible range.
172
+ */
173
+ export declare function resolveSplatBudget(override?: number, profile?: SplatDeviceProfile | undefined, options?: SplatBudgetOptions): number;
174
+ /**
175
+ * The budget to run at **while an immersive session presents**, given whatever
176
+ * budget the page is using outside one.
177
+ *
178
+ * Stereo is the cost, and stereo is a property of the *session*, not of the
179
+ * device: every splat is drawn twice, into two eye viewports that together
180
+ * exceed a 4K desktop, on a GPU that must hold 72–90 Hz or the compositor
181
+ * reprojects. That is true of a standalone headset and equally true of a
182
+ * desktop machine driving a tethered one - which is exactly the case device
183
+ * sniffing cannot see, since such a machine is a desktop right up until the
184
+ * moment it is not.
185
+ *
186
+ * Keying off presentation instead of identity is also the only approach that
187
+ * survives new hardware. `detectSplatDeviceProfile`'s `isHeadset` is a
188
+ * user-agent guess: it misses Apple Vision Pro outright (visionOS Safari
189
+ * presents as desktop Safari, so a headset would otherwise take the multi-
190
+ * million desktop budget) and misses every headset released after it is
191
+ * written. This function needs to know none of that.
192
+ *
193
+ * Apply it on `sessionstart` and restore the original on `sessionend` - via
194
+ * `BudgetGovernor.setBudget` when several meshes share a pool, or a streamed
195
+ * mesh's own budget setter otherwise.
196
+ *
197
+ * @param pageBudget - The budget in use outside the session, typically from
198
+ * {@link resolveSplatBudget}. Throws `RangeError` if not a positive finite
199
+ * number.
200
+ * @returns `pageBudget` lowered to the stereo ceiling; never raised - a device
201
+ * already rendering below the ceiling stays where it is.
202
+ */
203
+ export declare function resolveXrSplatBudget(pageBudget: number): number;
204
+ /**
205
+ * Raises `budget` far enough to hold a scene's finest level in full, when
206
+ * that level is small enough to be worth taking whole.
207
+ *
208
+ * The LOD budget assumes coarser levels are cheap *approximations* of the
209
+ * same surface, so trading detail for memory is nearly free. XGRIDS' LCC
210
+ * LCC breaks that assumption: its five levels are decimated alternatives
211
+ * whose splats merge into wide flat discs (correct at distance, streaked up
212
+ * close), so any budget shortfall costs visible quality - sub-chunking gives
213
+ * the scheduler ~128k-splat granularity to thin cells with, but a thinned
214
+ * fine level is still sparser than the capture intends. A capture whose
215
+ * finest level fits under the desktop ceiling is better shown whole: Casino's
216
+ * level 0 is 4.74M splats and sits under the 8M default.
217
+ *
218
+ * Mobile and fill-constrained desktops (integrated / fallback GPU class) are
219
+ * exempt: their cap is a fill-rate limit, not a sizing accident.
220
+ *
221
+ * @param budget - The resolved device budget.
222
+ * @param finestLevelSplats - Splats in the scene's finest level.
223
+ * @returns `budget`, or the finest level's size when that is the better fit.
224
+ */
225
+ export declare function liftBudgetToFinestLevel(budget: number, finestLevelSplats: number, profile?: SplatDeviceProfile | undefined): number;
226
+ /** Options for {@link estimateSplatPoolBytes}. */
227
+ export interface SplatPoolBytesOptions {
228
+ /**
229
+ * Pool texture precision, matching `SplatMeshOptions.poolFloatTextures`.
230
+ * `'float16'` halves centers and covarianceA. Default `'float32'`.
231
+ */
232
+ floatTextures?: 'float32' | 'float16';
233
+ /** Per-splat SH bands the pool allocates for. Default `0`. */
234
+ shBands?: 0 | 1 | 2 | 3;
235
+ /**
236
+ * Pool capacity as a multiple of `splats`. `StreamedSplatMesh` allocates
237
+ * 1.5× (the default here) so per-run row alignment and the
238
+ * append-before-remove window during LOD swaps have somewhere to go; pass
239
+ * `1.4` to model `experimentalStagedSwaps: false`, or `1` for a static mesh.
240
+ */
241
+ capacityFactor?: number;
242
+ /**
243
+ * Whether to include the CPU-side backing arrays. Default `true` - they are
244
+ * real host memory a device has to find, so the honest answer to "can I
245
+ * afford this ceiling" includes them.
246
+ */
247
+ includeCpuBacking?: boolean;
248
+ }
249
+ /**
250
+ * Estimates the memory a splat pool of `splats` costs.
251
+ *
252
+ * This exists to make a streamed mesh's `maxBudget` a computation rather than a
253
+ * guess. A pool is allocated **once, from the ceiling** and never
254
+ * grows, so several governed meshes cost the sum of their ceilings whatever the
255
+ * shared budget is set to - a `BudgetGovernor` redistributes *sharpness* within
256
+ * that envelope, it does not shrink it. Price the ceilings before choosing them:
257
+ *
258
+ * ```js
259
+ * // 1 main + 4 markers, each able to reach 1.5M splats
260
+ * const bytes = estimateSplatPoolBytes(4_000_000) + 4 * estimateSplatPoolBytes(1_500_000);
261
+ * ```
262
+ *
263
+ * Per splat of *capacity* (`splats × capacityFactor`), counted from what the
264
+ * constructor actually allocates:
265
+ *
266
+ * - **Pool textures** 52 B - centers RGBA32F 16, colors RGBA8 4,
267
+ * covarianceA RGBA32F 16, covarianceB RGBA32F 16. `'float16'` drops centers
268
+ * and covarianceA to 8 each (36 B); covarianceB stays float32 because it
269
+ * packs integer IDs.
270
+ * - **Packed SH** 16 B per `RGBA32UI` texture, `ceil(coefficients / 4)` of them
271
+ * - so 16 / 32 / 64 B at 1 / 2 / 3 bands.
272
+ * - **Sort storage** 16 B - the radix sorter's ping-pong key/value buffers.
273
+ * - **CPU backing** 68 B - the float32/uint8 arrays kept for partial uploads
274
+ * (always full precision, even under `'float16'`): centers 16, colors 4,
275
+ * covarianceA 16, covarianceB 16, plus four u32-per-splat arrays - draw-order
276
+ * indices, the active-list source index, the pool-slot map, and the picker's
277
+ * pool-index template (allocated lazily, counted because a picked scene pays
278
+ * it). Under `'float16'` add a further 16 B: the half-encoded texture images
279
+ * are held alongside the float32 backing, not instead of it, so that
280
+ * precision saves GPU bytes only. Included unless `includeCpuBacking` is
281
+ * `false`.
282
+ *
283
+ * The module header's "roughly 64 bytes per splat" rounds the GPU side of this.
284
+ *
285
+ * @param splats - The ceiling in splats (e.g. a mesh's `maxBudget`).
286
+ * @returns Estimated bytes. Indicative, not a device-memory guarantee: driver
287
+ * texture padding, staging allocations and the decoded-chunk CPU cache
288
+ * ({@link resolveCpuCacheBytes}) sit outside it.
289
+ * @throws {RangeError} if `splats` is not a positive finite number, or
290
+ * `capacityFactor` is not a finite number `>= 1`.
291
+ */
292
+ export declare function estimateSplatPoolBytes(splats: number, options?: SplatPoolBytesOptions): number;
293
+ /**
294
+ * Suggested ceiling for the renderer's pixel ratio on this device.
295
+ *
296
+ * Splat rendering is fragment-bound, so render resolution is one of the
297
+ * largest costs on a high-DPI phone: a 2.6x display renders ~7x the pixels of
298
+ * a 1x one, and every one of them blends the full depth-sorted splat stack.
299
+ * The library cannot apply this itself (it draws into a renderer the app
300
+ * owns), so it exports the policy - pass it to `renderer.setPixelRatio`:
301
+ *
302
+ * ```js
303
+ * renderer.setPixelRatio(Math.min(window.devicePixelRatio, recommendedMaxPixelRatio()));
304
+ * ```
305
+ *
306
+ * This is the *quality* ceiling. Viewers that offer a performance mode
307
+ * (SuperSplat halves mobile resolution in its own) should go lower still when
308
+ * it is on.
309
+ */
310
+ export declare function recommendedMaxPixelRatio(profile?: SplatDeviceProfile | undefined): number;
311
+ /**
312
+ * The Gaussian cutoff a `.rad` renders at, in standard deviations, or
313
+ * `undefined` to accept the device-wide default.
314
+ *
315
+ * Spark hard-codes `sqrt(8)` (≈2.83σ) for every device, and matching it is what
316
+ * makes a `.rad` look like Spark's render on a desktop. On a phone it is the
317
+ * wrong trade: rendering is fill-bound on mobile, so a format override bypasses
318
+ * the device policy that every other format accepts.
319
+ *
320
+ * Returning `undefined` on mobile lets `SplatMesh` apply its own 3σ cutoff and
321
+ * undersized-splat floor; `.rad` must not escape that mobile policy.
322
+ *
323
+ * @returns √8 on discrete desktop; `undefined` on fill-constrained devices,
324
+ * meaning "no format override".
325
+ */
326
+ export declare function recommendedRadMaxStdDev(profile?: SplatDeviceProfile | undefined): number | undefined;
327
+ /**
328
+ * Suggested WebXR framebuffer scale for this device. On three 0.185.x this is
329
+ * a **WebGL XR** policy: apply it once before the session starts with
330
+ * `renderer.xr.setFramebufferScaleFactor`. three's WebGPU XR path creates its
331
+ * `XRGPUBinding` projection layer at native scale and does not consume that
332
+ * setting, so WebGPU hosts should leave the native scale and use the presenting
333
+ * splat budget plus fixed foveation for runtime headroom instead.
334
+ *
335
+ * Splat rendering is fill-bound, and a headset's default framebuffer is
336
+ * already supersampled past its panels (~1680×1760 per eye on Quest 3):
337
+ * 0.8 cuts fragment work ~36% for a barely visible softening. Non-headset
338
+ * XR (desktop-tethered) keeps the native 1.0.
339
+ */
340
+ export declare function recommendedXrFramebufferScale(profile?: SplatDeviceProfile | undefined): number;
341
+ /** Inputs for {@link suggestAdaptivePixelRatio}. */
342
+ export interface AdaptivePixelRatioInput {
343
+ /** Latest wall-frame time in milliseconds. */
344
+ frameMs: number;
345
+ /** Currently applied pixel ratio. */
346
+ current: number;
347
+ /** Quality ceiling (typically {@link recommendedMaxPixelRatio}). */
348
+ max: number;
349
+ /** Floor; defaults to `1`. */
350
+ min?: number;
351
+ /** EMA of frame time from the previous call; omit on the first sample. */
352
+ emaMs?: number;
353
+ /**
354
+ * Comfortable frame time (ms). Below this (with headroom) the helper may
355
+ * step the ratio up. Default `18` (~55 fps).
356
+ */
357
+ targetFrameMs?: number;
358
+ /**
359
+ * Sustained frame time (ms) that triggers a step down. Default `22`
360
+ * (~45 fps) so brief spikes do not thrash the canvas size.
361
+ */
362
+ pressureFrameMs?: number;
363
+ }
364
+ /** Result of {@link suggestAdaptivePixelRatio}. */
365
+ export interface AdaptivePixelRatioResult {
366
+ /** Suggested pixel ratio after hysteresis (quarter steps). */
367
+ pixelRatio: number;
368
+ /** Updated EMA to pass back on the next call. */
369
+ emaMs: number;
370
+ }
371
+ /**
372
+ * Suggests a pixel ratio under frame-time pressure.
373
+ *
374
+ * The library cannot call `renderer.setPixelRatio` (the host owns the
375
+ * renderer); this is a pure policy helper. Pass the previous result's
376
+ * `emaMs` each frame for a stable EMA, and only re-size the canvas when
377
+ * `pixelRatio` changes.
378
+ *
379
+ * Steps are quarter-units with asymmetric thresholds (pressure to lower,
380
+ * comfortable headroom to raise) so the ratio does not oscillate.
381
+ */
382
+ export declare function suggestAdaptivePixelRatio(input: AdaptivePixelRatioInput): AdaptivePixelRatioResult;
383
+ /**
384
+ * Chooses a decoded-chunk CPU cache cap from the same safe device profile.
385
+ *
386
+ * **The unknown-memory fallback must not read as "tiny".** iOS Safari does not
387
+ * implement `navigator.deviceMemory` at all, so every iPhone lands on this
388
+ * branch - and an earlier version resolved it to 1 GiB, i.e. the 32 MiB floor,
389
+ * on hardware with 8 GiB. That is not a conservative guess, it is a wrong one,
390
+ * and it is invisible on `.rad`, whose page-table cache has its own floor.
391
+ *
392
+ * On streamed SOG there is no such floor and the cost is immediate: measured on
393
+ * an iPhone 15 Pro against `sandwijck`, the scheduler asked for 539,734 splats
394
+ * and the mesh could only hold 466,499 of them across 3 chunk files (desktop:
395
+ * 5), evicting continuously. The finest level is wanted *nearest the camera* and
396
+ * lives in the largest files, so those are what fail to stay resident - the
397
+ * middle of the view drops out and the scene renders as a donut.
398
+ *
399
+ * A device that declines to report its memory is far likelier to be a modern
400
+ * phone withholding a fingerprinting signal than an actual 1 GiB device, so the
401
+ * fallback assumes 4 GiB (128 MiB of cache). The floor stays for the genuinely
402
+ * profile-less case - SSR and Node tooling, which never stream a scene anyway.
403
+ */
404
+ export declare function resolveCpuCacheBytes(profile?: SplatDeviceProfile | undefined): number;
@@ -0,0 +1,10 @@
1
+ const z = 0.28209479177387814;
2
+ function A(c, s, H, L, M, _, b, j, k) {
3
+ const h = Math.hypot(_, b, j, k) || 1, m = _ / h, t = b / h, n = j / h, o = k / h, r = (1 - 2 * (n * n + o * o)) * H, p = 2 * (t * n - m * o) * L, w = 2 * (t * o + m * n) * M, C = 2 * (t * n + m * o) * H, S = (1 - 2 * (t * t + o * o)) * L, f = 2 * (n * o - m * t) * M, g = 2 * (t * o - m * n) * H, v = 2 * (n * o + m * t) * L, y = (1 - 2 * (t * t + n * n)) * M;
4
+ c[s * 6 + 0] = r * r + p * p + w * w, c[s * 6 + 1] = r * C + p * S + w * f, c[s * 6 + 2] = r * g + p * v + w * y, c[s * 6 + 3] = C * C + S * S + f * f, c[s * 6 + 4] = C * g + S * v + f * y, c[s * 6 + 5] = g * g + v * v + y * y;
5
+ }
6
+ export {
7
+ z as S,
8
+ A as w
9
+ };
10
+ //# sourceMappingURL=splat-data-BZM5dhhx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splat-data-BZM5dhhx.js","sources":["../src/lib/splat-data.ts"],"sourcesContent":["import type { SplatSourceFormat } from './loading';\n\n/**\n * CPU-side representation of a set of 3D Gaussians, ready for GPU upload.\n *\n * Everything is stored in flat typed arrays (struct-of-arrays) so the data\n * can be copied straight into textures and transferred to workers.\n */\nexport interface SplatData {\n /** Number of Gaussians. */\n readonly count: number;\n /** Gaussian centers, 3 floats (x, y, z) per splat. */\n readonly positions: Float32Array;\n /** Base color + opacity, 4 bytes (r, g, b, a) per splat. */\n readonly colors: Uint8Array;\n /**\n * 3D covariance matrices, 6 floats per splat: the upper triangle\n * (m00, m01, m02, m11, m12, m22) of the symmetric 3×3 matrix\n * Σ = R·S·Sᵀ·Rᵀ, precomputed from the per-splat scale and rotation.\n */\n readonly covariances: Float32Array;\n /** Higher-order spherical harmonics (view-dependent color), if present. */\n readonly sh?: SplatShData;\n /**\n * Per-splat (rather than palette-compressed) higher-order SH, as LCC `Quality`\n * delivers it. Unlike {@link sh}, this survives being appended into a\n * shared pool, because there is no per-file palette to merge.\n */\n readonly shPacked?: SplatPackedShData;\n /**\n * Scene was trained/exported with antialiasing (e.g. the SOG `antialias`\n * meta flag). When true, {@link SplatMesh} applies the Mip-Splatting 2D\n * filter (dilation + opacity compensation). Absent/false = classic 3DGS.\n */\n readonly antialias?: boolean;\n /**\n * Per-splat LOD-tree links for a streamed `.rad` chunk (the merged coarse\n * nodes and leaves are all present in {@link positions} etc.). The streamed\n * reader uses this to pick the rendered cut across chunks; other formats and\n * the one-shot path leave it undefined. See `parse-rad.ts` and `rad.ts`.\n */\n readonly radTree?: RadTreeData;\n /**\n * Per-splat parent LOD-node size for the frontier cut (`foveationMode:\n * 'frontier'`), sign-encoding leaf-ness: `> 0` internal node, `< 0` leaf,\n * `|value|` is the parent's world size (a large sentinel ≈ ∞ for a root or a\n * splat whose parent is not yet decoded). Attached by `RadFoveatedSource` as\n * chunks stream in; uploaded into `covarianceB.w`. See `docs/formats/rad-notes.md`.\n */\n readonly frontierParent?: Float32Array;\n /** Internal RAD SH codebook retained while a streamed `.rad` scene is set\n * up. It is passed back to later chunk decodes; consumers can ignore it. */\n readonly radShCodebook?: RadShCodebook;\n /**\n * The self-contained format this scene was decoded from, stamped by the\n * loaders ({@link loadScene}/{@link loadSceneFile}). {@link SplatMesh} reads\n * it to pick the `orientation: 'y-up'` correction. Absent on hand-built data\n * and streamed chunks, in which case no orientation is ever inferred.\n */\n readonly sourceFormat?: SplatSourceFormat;\n}\n\n/** Palette emitted once in chunk zero by Spark's clustered RAD SH encoding. */\nexport interface RadShCodebook {\n readonly bands: 1 | 2 | 3;\n /** RGB triples, coefficient-major and codebook-entry-major. */\n readonly coefficients: Float32Array;\n readonly count: number;\n}\n\n/** Per-splat LOD-tree links carried on a streamed `.rad` chunk's `SplatData`. */\nexport interface RadTreeData {\n /** Number of children of splat `i`; 0 marks a leaf. */\n readonly childCount: Uint16Array;\n /** Global index of splat `i`'s first child; its children are the contiguous\n * range `[childStart, childStart + childCount)`. */\n readonly childStart: Uint32Array;\n /**\n * LOD node size (world units) of splat `i` - Spark's `2·expansion·avg(scale)`,\n * the radius the runtime cut projects to a screen size. Drives foveation\n * (see `docs/formats/rad-notes.md` M14.6): `pixel_scale = size / distance`.\n */\n readonly size: Float32Array;\n}\n\n/**\n * Palette-compressed higher-order SH coefficients, as in the SOG format:\n * each splat references one of up to 65,536 palette entries (its \"label\"),\n * and each entry holds the coefficients for all three color channels.\n */\nexport interface SplatShData {\n /** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */\n readonly bands: number;\n /** Per-splat palette index, `count` entries. */\n readonly labels: Uint32Array;\n /**\n * Dequantized palette coefficients as an RGBA float image in the SOG\n * centroids layout: palette entry n, coefficient c lives at column\n * (n % 64) · coefficientCount + c, row ⌊n / 64⌋; the R, G, B channels\n * hold that coefficient for the red, green and blue SH respectively.\n * Uploadable directly as an RGBA32F texture.\n */\n readonly palette: Float32Array;\n readonly paletteWidth: number;\n readonly paletteHeight: number;\n}\n\n/**\n * Per-splat higher-order SH coefficients, still in their source packing:\n * one `uint32` per coefficient holding all three channels (R: bits 0-10,\n * G: bits 11-20, B: bits 21-31), dequantized across {@link range}.\n *\n * The words are kept packed all the way to the GPU - the shader unpacks them.\n * Expanding to floats would cost 45 floats per splat (~1.15 GB over a 6M-splat\n * pool) where the packed form costs 64 bytes (~384 MB) and is lossless.\n */\nexport interface SplatPackedShData {\n /** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */\n readonly bands: 1 | 2 | 3;\n /** `count * coefficients` words, splat-major. */\n readonly packed: Uint32Array;\n /** Per-channel dequantization range shared by every coefficient. */\n readonly range: {\n readonly min: readonly [number, number, number];\n readonly max: readonly [number, number, number];\n };\n}\n\n/** First-order spherical harmonics constant: Y₀₀ = 1 / (2√π). */\nexport const SH_C0 = 0.28209479177387814;\n\n/**\n * Folds a splat's linear scale and rotation quaternion into the upper\n * triangle of its 3D covariance matrix Σ = R·S·Sᵀ·Rᵀ (the 3DGS paper's\n * parameterization), written to `out` at `index * 6`.\n *\n * The quaternion (w, x, y, z) does not need to be normalized.\n */\nexport function writeCovariance(\n out: Float32Array,\n index: number,\n scaleX: number,\n scaleY: number,\n scaleZ: number,\n qw: number,\n qx: number,\n qy: number,\n qz: number,\n): void {\n const qLength = Math.hypot(qw, qx, qy, qz) || 1;\n const w = qw / qLength;\n const x = qx / qLength;\n const y = qy / qLength;\n const z = qz / qLength;\n\n // M = R·S (rotation matrix from the quaternion, columns scaled).\n const m00 = (1 - 2 * (y * y + z * z)) * scaleX;\n const m01 = 2 * (x * y - w * z) * scaleY;\n const m02 = 2 * (x * z + w * y) * scaleZ;\n const m10 = 2 * (x * y + w * z) * scaleX;\n const m11 = (1 - 2 * (x * x + z * z)) * scaleY;\n const m12 = 2 * (y * z - w * x) * scaleZ;\n const m20 = 2 * (x * z - w * y) * scaleX;\n const m21 = 2 * (y * z + w * x) * scaleY;\n const m22 = (1 - 2 * (x * x + y * y)) * scaleZ;\n\n // Σ = M·Mᵀ is symmetric; store its upper triangle.\n out[index * 6 + 0] = m00 * m00 + m01 * m01 + m02 * m02;\n out[index * 6 + 1] = m00 * m10 + m01 * m11 + m02 * m12;\n out[index * 6 + 2] = m00 * m20 + m01 * m21 + m02 * m22;\n out[index * 6 + 3] = m10 * m10 + m11 * m11 + m12 * m12;\n out[index * 6 + 4] = m10 * m20 + m11 * m21 + m12 * m22;\n out[index * 6 + 5] = m20 * m20 + m21 * m21 + m22 * m22;\n}\n\n/**\n * Transforms an existing covariance Σ (upper triangle `m00,m01,m02,m11,m12,m22`)\n * by the 3×3 linear part `A` of a world matrix, writing Σ' = A·Σ·Aᵀ back as an\n * upper triangle. This is how a Gaussian's shape maps into a parent frame when a\n * splat cloud is placed with a world transform - the rotation/scale portion of\n * that transform (translation does not affect covariance) applied to the shape.\n *\n * Unlike {@link writeCovariance}, which *builds* Σ from a splat's scale and\n * rotation, this operates on an already-folded Σ. Non-uniform scale and shear in\n * `A` are handled exactly (the result is still a valid symmetric covariance).\n *\n * `a` is the linear part in **column-major** order - the first 9 elements of a\n * {@link https://threejs.org/docs/#api/en/math/Matrix4 | THREE.Matrix4}'s\n * `elements` layout for a matrix with no projective row, i.e.\n * `[a00, a10, a20, a01, a11, a21, a02, a12, a22]`. Reading `Matrix4.elements`\n * directly (ignoring the translation column and bottom row) yields exactly this.\n */\nexport function transformCovariance(\n out: Float32Array,\n outIndex: number,\n cov: Float32Array,\n covIndex: number,\n a: ArrayLike<number>,\n): void {\n // A, column-major: a[col*4 ... ] would be Matrix4; here we take the linear 3×3\n // as elements[0..2] = column 0, [3..5] would skip - but Matrix4 stores a 4×4,\n // so callers pass the compacted 9-element linear part described above.\n const a00 = a[0] as number;\n const a10 = a[1] as number;\n const a20 = a[2] as number;\n const a01 = a[3] as number;\n const a11 = a[4] as number;\n const a21 = a[5] as number;\n const a02 = a[6] as number;\n const a12 = a[7] as number;\n const a22 = a[8] as number;\n\n // Σ (symmetric) from the upper triangle.\n const s00 = cov[covIndex * 6 + 0] as number;\n const s01 = cov[covIndex * 6 + 1] as number;\n const s02 = cov[covIndex * 6 + 2] as number;\n const s11 = cov[covIndex * 6 + 3] as number;\n const s12 = cov[covIndex * 6 + 4] as number;\n const s22 = cov[covIndex * 6 + 5] as number;\n\n // T = A·Σ (3×3). Rows of A dotted with columns of the symmetric Σ.\n const t00 = a00 * s00 + a01 * s01 + a02 * s02;\n const t01 = a00 * s01 + a01 * s11 + a02 * s12;\n const t02 = a00 * s02 + a01 * s12 + a02 * s22;\n const t10 = a10 * s00 + a11 * s01 + a12 * s02;\n const t11 = a10 * s01 + a11 * s11 + a12 * s12;\n const t12 = a10 * s02 + a11 * s12 + a12 * s22;\n const t20 = a20 * s00 + a21 * s01 + a22 * s02;\n const t21 = a20 * s01 + a21 * s11 + a22 * s12;\n const t22 = a20 * s02 + a21 * s12 + a22 * s22;\n\n // Σ' = T·Aᵀ; store the upper triangle (Σ' is symmetric).\n out[outIndex * 6 + 0] = t00 * a00 + t01 * a01 + t02 * a02;\n out[outIndex * 6 + 1] = t00 * a10 + t01 * a11 + t02 * a12;\n out[outIndex * 6 + 2] = t00 * a20 + t01 * a21 + t02 * a22;\n out[outIndex * 6 + 3] = t10 * a10 + t11 * a11 + t12 * a12;\n out[outIndex * 6 + 4] = t10 * a20 + t11 * a21 + t12 * a22;\n out[outIndex * 6 + 5] = t20 * a20 + t21 * a21 + t22 * a22;\n}\n"],"names":["SH_C0","writeCovariance","out","index","scaleX","scaleY","scaleZ","qw","qx","qy","qz","qLength","w","x","y","z","m00","m01","m02","m10","m11","m12","m20","m21","m22"],"mappings":"AAiIO,MAAMA,IAAQ;AASd,SAASC,EACdC,GACAC,GACAC,GACAC,GACAC,GACAC,GACAC,GACAC,GACAC,GACM;AACN,QAAMC,IAAU,KAAK,MAAMJ,GAAIC,GAAIC,GAAIC,CAAE,KAAK,GACxCE,IAAIL,IAAKI,GACTE,IAAIL,IAAKG,GACTG,IAAIL,IAAKE,GACTI,IAAIL,IAAKC,GAGTK,KAAO,IAAI,KAAKF,IAAIA,IAAIC,IAAIA,MAAMX,GAClCa,IAAM,KAAKJ,IAAIC,IAAIF,IAAIG,KAAKV,GAC5Ba,IAAM,KAAKL,IAAIE,IAAIH,IAAIE,KAAKR,GAC5Ba,IAAM,KAAKN,IAAIC,IAAIF,IAAIG,KAAKX,GAC5BgB,KAAO,IAAI,KAAKP,IAAIA,IAAIE,IAAIA,MAAMV,GAClCgB,IAAM,KAAKP,IAAIC,IAAIH,IAAIC,KAAKP,GAC5BgB,IAAM,KAAKT,IAAIE,IAAIH,IAAIE,KAAKV,GAC5BmB,IAAM,KAAKT,IAAIC,IAAIH,IAAIC,KAAKR,GAC5BmB,KAAO,IAAI,KAAKX,IAAIA,IAAIC,IAAIA,MAAMR;AAGxC,EAAAJ,EAAIC,IAAQ,IAAI,CAAC,IAAIa,IAAMA,IAAMC,IAAMA,IAAMC,IAAMA,GACnDhB,EAAIC,IAAQ,IAAI,CAAC,IAAIa,IAAMG,IAAMF,IAAMG,IAAMF,IAAMG,GACnDnB,EAAIC,IAAQ,IAAI,CAAC,IAAIa,IAAMM,IAAML,IAAMM,IAAML,IAAMM,GACnDtB,EAAIC,IAAQ,IAAI,CAAC,IAAIgB,IAAMA,IAAMC,IAAMA,IAAMC,IAAMA,GACnDnB,EAAIC,IAAQ,IAAI,CAAC,IAAIgB,IAAMG,IAAMF,IAAMG,IAAMF,IAAMG,GACnDtB,EAAIC,IAAQ,IAAI,CAAC,IAAImB,IAAMA,IAAMC,IAAMA,IAAMC,IAAMA;AACrD;"}
@@ -0,0 +1,150 @@
1
+ import type { SplatSourceFormat } from './loading';
2
+ /**
3
+ * CPU-side representation of a set of 3D Gaussians, ready for GPU upload.
4
+ *
5
+ * Everything is stored in flat typed arrays (struct-of-arrays) so the data
6
+ * can be copied straight into textures and transferred to workers.
7
+ */
8
+ export interface SplatData {
9
+ /** Number of Gaussians. */
10
+ readonly count: number;
11
+ /** Gaussian centers, 3 floats (x, y, z) per splat. */
12
+ readonly positions: Float32Array;
13
+ /** Base color + opacity, 4 bytes (r, g, b, a) per splat. */
14
+ readonly colors: Uint8Array;
15
+ /**
16
+ * 3D covariance matrices, 6 floats per splat: the upper triangle
17
+ * (m00, m01, m02, m11, m12, m22) of the symmetric 3×3 matrix
18
+ * Σ = R·S·Sᵀ·Rᵀ, precomputed from the per-splat scale and rotation.
19
+ */
20
+ readonly covariances: Float32Array;
21
+ /** Higher-order spherical harmonics (view-dependent color), if present. */
22
+ readonly sh?: SplatShData;
23
+ /**
24
+ * Per-splat (rather than palette-compressed) higher-order SH, as LCC `Quality`
25
+ * delivers it. Unlike {@link sh}, this survives being appended into a
26
+ * shared pool, because there is no per-file palette to merge.
27
+ */
28
+ readonly shPacked?: SplatPackedShData;
29
+ /**
30
+ * Scene was trained/exported with antialiasing (e.g. the SOG `antialias`
31
+ * meta flag). When true, {@link SplatMesh} applies the Mip-Splatting 2D
32
+ * filter (dilation + opacity compensation). Absent/false = classic 3DGS.
33
+ */
34
+ readonly antialias?: boolean;
35
+ /**
36
+ * Per-splat LOD-tree links for a streamed `.rad` chunk (the merged coarse
37
+ * nodes and leaves are all present in {@link positions} etc.). The streamed
38
+ * reader uses this to pick the rendered cut across chunks; other formats and
39
+ * the one-shot path leave it undefined. See `parse-rad.ts` and `rad.ts`.
40
+ */
41
+ readonly radTree?: RadTreeData;
42
+ /**
43
+ * Per-splat parent LOD-node size for the frontier cut (`foveationMode:
44
+ * 'frontier'`), sign-encoding leaf-ness: `> 0` internal node, `< 0` leaf,
45
+ * `|value|` is the parent's world size (a large sentinel ≈ ∞ for a root or a
46
+ * splat whose parent is not yet decoded). Attached by `RadFoveatedSource` as
47
+ * chunks stream in; uploaded into `covarianceB.w`. See `docs/formats/rad-notes.md`.
48
+ */
49
+ readonly frontierParent?: Float32Array;
50
+ /** Internal RAD SH codebook retained while a streamed `.rad` scene is set
51
+ * up. It is passed back to later chunk decodes; consumers can ignore it. */
52
+ readonly radShCodebook?: RadShCodebook;
53
+ /**
54
+ * The self-contained format this scene was decoded from, stamped by the
55
+ * loaders ({@link loadScene}/{@link loadSceneFile}). {@link SplatMesh} reads
56
+ * it to pick the `orientation: 'y-up'` correction. Absent on hand-built data
57
+ * and streamed chunks, in which case no orientation is ever inferred.
58
+ */
59
+ readonly sourceFormat?: SplatSourceFormat;
60
+ }
61
+ /** Palette emitted once in chunk zero by Spark's clustered RAD SH encoding. */
62
+ export interface RadShCodebook {
63
+ readonly bands: 1 | 2 | 3;
64
+ /** RGB triples, coefficient-major and codebook-entry-major. */
65
+ readonly coefficients: Float32Array;
66
+ readonly count: number;
67
+ }
68
+ /** Per-splat LOD-tree links carried on a streamed `.rad` chunk's `SplatData`. */
69
+ export interface RadTreeData {
70
+ /** Number of children of splat `i`; 0 marks a leaf. */
71
+ readonly childCount: Uint16Array;
72
+ /** Global index of splat `i`'s first child; its children are the contiguous
73
+ * range `[childStart, childStart + childCount)`. */
74
+ readonly childStart: Uint32Array;
75
+ /**
76
+ * LOD node size (world units) of splat `i` - Spark's `2·expansion·avg(scale)`,
77
+ * the radius the runtime cut projects to a screen size. Drives foveation
78
+ * (see `docs/formats/rad-notes.md` M14.6): `pixel_scale = size / distance`.
79
+ */
80
+ readonly size: Float32Array;
81
+ }
82
+ /**
83
+ * Palette-compressed higher-order SH coefficients, as in the SOG format:
84
+ * each splat references one of up to 65,536 palette entries (its "label"),
85
+ * and each entry holds the coefficients for all three color channels.
86
+ */
87
+ export interface SplatShData {
88
+ /** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */
89
+ readonly bands: number;
90
+ /** Per-splat palette index, `count` entries. */
91
+ readonly labels: Uint32Array;
92
+ /**
93
+ * Dequantized palette coefficients as an RGBA float image in the SOG
94
+ * centroids layout: palette entry n, coefficient c lives at column
95
+ * (n % 64) · coefficientCount + c, row ⌊n / 64⌋; the R, G, B channels
96
+ * hold that coefficient for the red, green and blue SH respectively.
97
+ * Uploadable directly as an RGBA32F texture.
98
+ */
99
+ readonly palette: Float32Array;
100
+ readonly paletteWidth: number;
101
+ readonly paletteHeight: number;
102
+ }
103
+ /**
104
+ * Per-splat higher-order SH coefficients, still in their source packing:
105
+ * one `uint32` per coefficient holding all three channels (R: bits 0-10,
106
+ * G: bits 11-20, B: bits 21-31), dequantized across {@link range}.
107
+ *
108
+ * The words are kept packed all the way to the GPU - the shader unpacks them.
109
+ * Expanding to floats would cost 45 floats per splat (~1.15 GB over a 6M-splat
110
+ * pool) where the packed form costs 64 bytes (~384 MB) and is lossless.
111
+ */
112
+ export interface SplatPackedShData {
113
+ /** SH bands beyond the DC term: 1, 2 or 3 → 3, 8 or 15 coefficients. */
114
+ readonly bands: 1 | 2 | 3;
115
+ /** `count * coefficients` words, splat-major. */
116
+ readonly packed: Uint32Array;
117
+ /** Per-channel dequantization range shared by every coefficient. */
118
+ readonly range: {
119
+ readonly min: readonly [number, number, number];
120
+ readonly max: readonly [number, number, number];
121
+ };
122
+ }
123
+ /** First-order spherical harmonics constant: Y₀₀ = 1 / (2√π). */
124
+ export declare const SH_C0 = 0.28209479177387814;
125
+ /**
126
+ * Folds a splat's linear scale and rotation quaternion into the upper
127
+ * triangle of its 3D covariance matrix Σ = R·S·Sᵀ·Rᵀ (the 3DGS paper's
128
+ * parameterization), written to `out` at `index * 6`.
129
+ *
130
+ * The quaternion (w, x, y, z) does not need to be normalized.
131
+ */
132
+ export declare function writeCovariance(out: Float32Array, index: number, scaleX: number, scaleY: number, scaleZ: number, qw: number, qx: number, qy: number, qz: number): void;
133
+ /**
134
+ * Transforms an existing covariance Σ (upper triangle `m00,m01,m02,m11,m12,m22`)
135
+ * by the 3×3 linear part `A` of a world matrix, writing Σ' = A·Σ·Aᵀ back as an
136
+ * upper triangle. This is how a Gaussian's shape maps into a parent frame when a
137
+ * splat cloud is placed with a world transform - the rotation/scale portion of
138
+ * that transform (translation does not affect covariance) applied to the shape.
139
+ *
140
+ * Unlike {@link writeCovariance}, which *builds* Σ from a splat's scale and
141
+ * rotation, this operates on an already-folded Σ. Non-uniform scale and shear in
142
+ * `A` are handled exactly (the result is still a valid symmetric covariance).
143
+ *
144
+ * `a` is the linear part in **column-major** order - the first 9 elements of a
145
+ * {@link https://threejs.org/docs/#api/en/math/Matrix4 | THREE.Matrix4}'s
146
+ * `elements` layout for a matrix with no projective row, i.e.
147
+ * `[a00, a10, a20, a01, a11, a21, a02, a12, a22]`. Reading `Matrix4.elements`
148
+ * directly (ignoring the translation column and bottom row) yields exactly this.
149
+ */
150
+ export declare function transformCovariance(out: Float32Array, outIndex: number, cov: Float32Array, covIndex: number, a: ArrayLike<number>): void;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 24-bit RGB packing for linear view-space depth used by GPU splat picking.
3
+ *
4
+ * Depth is stored as a normalized value in [0, 1] mapped across the camera
5
+ * near/far range, then quantized into three 8-bit channels (R = high byte).
6
+ * This matches an RGBA8 render target without a depth-texture readback path.
7
+ */
8
+ import * as THREE from 'three/webgpu';
9
+ /**
10
+ * Packs a normalized depth in [0, 1] into 8-bit RGB (high → low).
11
+ * Values outside [0, 1] are clamped.
12
+ */
13
+ export declare function packNormalizedDepth(normalized: number): {
14
+ r: number;
15
+ g: number;
16
+ b: number;
17
+ };
18
+ /**
19
+ * Unpacks 8-bit RGB channels back to a normalized depth in [0, 1].
20
+ */
21
+ export declare function unpackNormalizedDepth(r: number, g: number, b: number): number;
22
+ /**
23
+ * Maps a positive linear view-space depth (OpenGL: −z) into [0, 1] using the
24
+ * camera near/far plane. Clamps to the range.
25
+ */
26
+ export declare function normalizeViewDepth(viewDepth: number, near: number, far: number): number;
27
+ /**
28
+ * Inverse of {@link normalizeViewDepth}.
29
+ */
30
+ export declare function denormalizeViewDepth(normalized: number, near: number, far: number): number;
31
+ /**
32
+ * Reconstructs a world-space hit from NDC and a positive view-space depth
33
+ * (−z in OpenGL camera space). Writes into `outPoint` and returns camera
34
+ * distance. The camera must have an up-to-date world matrix.
35
+ */
36
+ export declare function unprojectViewDepth(ndcX: number, ndcY: number, viewDepth: number, camera: THREE.Camera, outPoint: THREE.Vector3): {
37
+ point: THREE.Vector3;
38
+ distance: number;
39
+ };